Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

同一アプリをWeb版とElectron版の両方で作る方法

皆さん、こんにちはmiyasakagoです。
この記事はNode.js_2 Advent Calendar 2018 - Qiitaの23日目です。

0)はじめに

ここ2年くらいで私自身、Electronを実案件の中で使う機会が増えているのですが、比較的遭遇するのは、同一アプリでWeb版もElectron版(デスクトップアプリ)も両方ほしい、というようなシチュエーションです。
f:id:acro-engineer:20181224200843p:plain:h250
例えば、Electronベースで開発されているアプリの一つにSlackがありますが、SlackはWebアプリとしてブラウザ上から利用することも、デスクトップアプリとして利用することも可能になっていますよね。

こういうことを実現したいと思ったとき、どういう方法があるのかいくつか選択肢があり、迷いました。
少し探してもあまり情報がなかったため、いろいろ調べ自分なりに整理してみました。

1)Webアプリを先に作り、Electron上からURL呼び出しする方法
2)Webアプリを先に作り、Electronに移植する方法
3)Electronアプリを先に作り、Webアプリに移植する方法

では、詳細を見ていきましょう。

1)Webアプリを先に作り、Electron上からURL呼び出しする方法

1. 概要

タイトルの通り、Webアプリを開発した後、そのWebページをElectronのウィンドウ上に読み込み動作させる、という方法です。

Electronには、外部のWebページを読み込み、デスクトップアプリ上で動作させる便利な機能があります。
その機能を活用した方法になります。(本機能の概要は後述します。)

2. メリット/デメリット

この方法であれば、WebアプリのソースコードをElectron側に移植するような作業は発生せず、開発はWebアプリ側に注力できます。
一方、制約も出てきます。以下に整理します。

  • メリット
    • 開発コストを抑えられ、納期も短くできる。
    • アプリのバージョンアップが楽。わざわざ更新プログラムを配信してインストールさせなくても、Webアプリを更新すれば、自動的にElectronアプリも更新される。
    • Web版、Electron版それぞれで、同じようなソースコードを2重管理する必要がない。
  • ・デメリット
    • インターネット環境での動作が前提になる。そのため、アプリの要件として許容できるかどうか、先に確認しておく必要がある。
    • Electronのネイティブアプリ用の機能(例:ローカルファイルへのアクセス等)の利用に制限が生まれる。

3. 実現方法

このブログでは、上記の実装方法の詳細までは解説しませんが、2つの代表的な方法の紹介だけ簡単にしておきます。

a. webviewを利用する
  • webviewは、外部のWebページを読み込み、Electronのアプリ上に表示する機能。
  • webviewタグの使い方はHTMLの標準タグであるiframeに似ている。
  • Electronのプロセスとwebviewに読み込んだWebページとの間で情報のやり取りが可能であったりと、iframeには無い便利な機能性を持っている。
  • 本格利用を考える場合、webviewはChroniumが維持管理をしている為、Electronの要望通りにバグ修正が進まないという課題がある。

webviewでできることや実装方法については、以下のページなどに詳しく書かれています。
qiita.com

b. BrowserViewを利用する
  • BrowserViewは、webviewに替わる方法として実装されたもの。
  • webviewと違い、BrowserViewはElectronが維持管理をしている為、バグ修正が進まないリスクを減らせる。
  • 一方、まだwebviewほど機能が充実していないので、その点を踏まえた選定が必要。

BrowserViewの基本的な使い方については、
今年のAdvent Calendarでも紹介されていますので、ここでは割愛します。
qiita.com

参考までに

現在のSlackのデスクトップアプリ(version3.0以降)は、BrowserViewを利用して実現されている代表例の一つです。
なお、Slackは、version2.xまではwebviewを用いており、3.0へのバージョンアップ時にBrowserViewに切り替えたことが、Charlie Hess氏の以下のブログで紹介されています。
slack.engineering

webviewとBrowserViewの選定をする上では、一度読んでおくことをおすすめします。

2)Webアプリを先に作り、Electronに移植する方法

1. 概要

「1)Webアプリを先に作り、Electron上からURL呼び出しする方法」との違いは、Webアプリを作った後にソースコードをElectronに移植する点です。

f:id:acro-engineer:20181224194201p:plain:w500

移植を前提で考える場合、サーバサイドとクライアントサイドは疎結合にしておくことが重要であるため、SPA(シングルページアプリケーション)として開発しておくことをおすすめします。

また、サーバサイドをNode.jsで開発しておけば、ElectronのMainプロセスにサーバサイドのロジックも移植しやすくなるためおすすめです。
逆に、サーバサイドはAPIサーバとしてElectronには移植せず、Electronアプリから呼び出す構成にしても良いでしょう。

2. メリット/デメリット

  • メリット
    • 同じくWebアプリを先行開発する「1)Webアプリを先に作り、Electron上からURL呼び出しする方法」に比べ、Electronアプリへの移植後に、Electronのネイティブアプリ用の機能を追加することが容易。
    • Webアプリ先行であるにも関わらず、移植した結果としてオフラインでも動作するデスクトップアプリを作ることが可能。(※ただし、クライアント-サーバ間の通信部分などは改修する必要がある)
  • デメリット
    • 同じくWebアプリを先行開発する「1)Webアプリを先に作り、Electron上からURL呼び出しする方法」に比べ、移植のコストがかかる。
    • Web版とElectron版でソースコードが2重管理されることになるため、メンテナンスコストが高い。

3)Electronアプリを先に作り、Webアプリに移植する方法

1. 概要

「2)Webアプリを先に作り、Electronに移植する方法」の逆方向への移植を前提とした方法です。

f:id:acro-engineer:20181224194505p:plain:w500

前提として、ElectronアプリはNode.jsによって実行され(Mainプロセスと呼ばれる)、画面描画部分はHTML5、CSS3、JavaScriptによってChromium上で動作します(Rendererプロセスと呼ばれる)。
そのため、利用言語の関係上、ElectronアプリはWebアプリへの移植をしやすい構成になっていると言えます。

2. メリット/デメリット

  • メリット
    • Electronアプリを先に作るため、Electronのネイティブアプリ用の機能(例:ローカルファイルへのアクセス等)の利用に制約が生まれない。
    • そこまで意識せずとも、サーバサイドへの依存を減らした作りになりやすく、オフラインでも動作するデスクトップアプリを作りやすい。
  • デメリット
    • Electron上では、Mainプロセス(Node.jsで動作)とRendererプロセス(Chromium上で動作)はIPC(Inter-process Communication:プロセス間通信)による通信を行う為、その通信部分はそのままWebに移植はできない。
    • Electronのネイティブアプリ用の機能は移植できない為、Web移植時に交換しやすいクラス設計をしておくことが重要。
    • Web版とElectron版でソースコードが2重管理されることになるため、メンテナンスコストが高い。

4)まとめ

ここまで3つの方法を紹介しましたので、メリット/デメリットをまとめます。

No. 方法 メリット デメリット
1 Webアプリを先に作り、
Electron上からURL呼び出しする方法
・開発コストを抑えられ、納期も短くできる
・アプリのバージョンアップが楽
ソースコードの2重管理が不要
・デスクトップ版もインターネット環境での動作が前提になる
・ネイティブアプリ用の機能の利用に制限が生まれる
2 Webアプリを先に作り、
Electronに移植する方法
・ネイティブアプリ用の機能を追加することが容易
・オフラインでも動作するデスクトップアプリを作ることが可能
・1の方法に比べ、移植のコストがかかる
ソースコードが2重管理されることになる
3 Electronアプリを先に作り、
Webアプリに移植する方法
・ネイティブアプリ用の機能の利用に制約が生まれない
・オフラインでも動作するデスクトップアプリを作りやすい
・IPCによる通信部分はそのままWebに移植はできない
・ネイティブアプリ用の機能は移植できない
ソースコードが2重管理されることになる


最後に、上記のどの方法を採用するか決める上で、私が考えている判断基準を書いておきます。

  1. Webアプリ、デスクトップアプリのどちらをメインにするのか
  2. デスクトップアプリにてネイティブ機能の利用はどの程度求められるか
  3. 開発にコストと期間をどれだけ割り当てることができるか

これらの質問への回答とその優先度によって、採用する方法は概ね判断できると考えています。
例えば、分かりやすい例では以下のような具合です。

  • ネイティブ機能はあまり使う予定はなく、短期間での開発が求められる。
    →「1)Webアプリを先に作り、Electron上からURL呼び出しする方法」が良さそう
  • Webアプリメインだが、デスクトップアプリではネイティブ機能もしっかり使いたい。
    →「2)Webアプリを先に作り、Electronに移植する方法」が良さそう
  • デスクトップアプリがメインで、ネイティブ機能をしっかり使いたい。
    →「3)Electronアプリを先に作り、Webアプリに移植する方法」が良さそう

私のように「Webアプリもデスクトップアプリもほしい」というシチュエーションになったら、是非、参考にしてみてもらえたら幸いです。

それでは!

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
自社製品の開発から活用方法の提案まで関わりたいフロントエンジニア募集! - Acroquest Technology株式会社のWeb エンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

今日から使えるChainerを使用する際に便利な5つのトリック

メリークリスマス。@tereka114です。
この記事は「Chainer/CuPy Advent Calendar 2018」アドベントカレンダーの24日目です。

qiita.com

Chainerを利用する際の便利なトリックをクリスマスプレゼント代わりにご紹介します。
Chainerは非常に使いやすい良いフレームワークだと思います。
使っていく中で、デバッグやパラメータのチェック、あるいは既存モデルの利用など、かゆいことが必要になる時があり、その方法を調べる事が多くありました。

Chainerはそのようなことにもサポートしており、大変助かっています。
せっかくなので、使ってきたトリックをここで紹介します。

※Chainerのバージョンは5.1.0を用いています。

1. DEBUGモード

ニューラルネットワークは学習した結果、出力が常にNaNになることがあります。
NaNになる原因はゼロ除算や値が高すぎてinfになるなど様々です。
一つ言えることとして、この原因特定は相当厄介です。特定のために、まずは、いつ、どこで発生したのかを探る必要があります。
Chainerが標準で搭載しているDEBUGモードを利用することにより、NaNを検出できます。

例えば、次の例を見てみましょう。意図的にl1レイヤーの出力のあとに、NaNとなるよう変数を代入しています。

import chainer
import chainer.links as L
import numpy as np

class MLP(chainer.Chain):
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.l1 = L.Linear(2, 2)
            self.l2 = L.Linear(2, 2)

    def __call__(self, x):
        h = self.l1(x)
        h.data[0] = np.nan
        return self.l2(h)


x = np.array([[10, 10]], dtype=np.float32)
model = MLP()
print (model(x))

計算の出力は次の通りになります。

variable([[nan nan]])

今回の例は直接埋め込んでもいるので、ソースを見れば一目瞭然です。
ただし、Residual Networkのように層の数が2桁以上で構成されているネットワークを解析するのは至難の技です。

Chainerでは標準でDEBUG機能を持っています。
各レイヤーの出力にNaNが存在するかを検知します。使い方は次の通りです。環境変数「CHAINER_DEBUG」を1にします。

CHAINER_DEBUG=1 python debug.py 

出力結果は次の通りです。NaNを検出すると、Tracebackが出力されます。
Tracebackを確認するとself.l2で検知していることがわかります。
これによりself.l2を呼び出している付近を確認すれば良いと判断ができます。

Traceback (most recent call last):
  File "debug.py", line 24, in <module>
    print (model(x))
  File "debug.py", line 19, in __call__
★    return self.l2(h) ★
  File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/link.py", line 242, in __call__
    out = forward(*args, **kwargs)
  File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/links/connection/linear.py", line 138, in forward
    return linear.linear(x, self.W, self.b, n_batch_axes=n_batch_axes)
  File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/functions/connection/linear.py", line 289, in linear
    y, = LinearFunction().apply(args)
  File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/function_node.py", line 288, in apply
    raise RuntimeError(msg)
RuntimeError: NaN is detected on forward computation of LinearFunction

2. 問題のあるパラメータチェック

1のDEBUG_MODEではNaNを検出しました。
このDEBUG_MODEはあくまで各層の出力のチェックのみです。そのため、層のパラメータのどこに影響して得られたかは不明です。
当たり前ですが、数百万の数もあるパラメータを目視で確認するのは難しいです。

そのため、NaNのパラメータを再帰的に探索し、出力するコードを作成しました。
次の関数を使えば出力可能です。モデルを引数として与えれば、パラメータを出力します。

def parameter_check(model):
    for child in model.children():
        if isinstance(child, chainer.link.Link):
            for name, param in child.namedparams():
                # NaNの判定確認
                if np.isnan(param.data).any():
                    print (name)

この関数を試したコードは次のコードです。
Chainの中にChainを作り、sub_l1の変数のWにNaNを代入しました。

import chainer
import chainer.links as L
import numpy as np


class SubMLP(chainer.link.Chain):
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.sub_l1 = L.Linear(2, 2)
            self.sub_l2 = L.Linear(2, 2)

    def __call__(self, x):
        h = self.l1(x)
        return self.l2(h)


class MLP(chainer.link.Chain):
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.l1 = L.Linear(2, 2)
            self.sub_mlp = SubMLP()
            self.l2 = L.Linear(2, 2)

    def __call__(self, x):
        h = self.sub_mlp(self.l1(x))
        return self.l2(h)


def parameter_check(model):
    for child in model.children():
        if isinstance(child, chainer.link.Link):
            for name, param in child.namedparams():
                # NaNの判定確認
                if np.isnan(param.data).any():
                    print (name)


model = MLP()
model.sub_mlp.sub_l1.W.data[0] = np.nan
parameter_check(model)

出力は「/sub_l1/W」となるため、発見できています。

3. 再現性の確保

CuDNNを利用する場合、ニューラルネットワークの計算では再現性を確保できないことがあります。
CuDNNを用いて計算した場合は計算順序が担保されないことが知られています。
そのため、計算による誤差が発生し、最終的な計算結果が計算のたびに変化します。

これは、大きく精度に影響する事象ではありません。
ただし、これはデバッグ時に対処に困ることがあります。
デバッグでは、問題事象が再現できなければ、修正できたのか否かの解析が難しくなります。

このCuDNNの動作を決定的に動かす設定があります。
v2以前では、Convolution系のクラスにdetermistic引数がありましたが、v3以降では、chainer.configで設定可能です。
変更するには次のコードを実行しましょう。

chainer.config.cudnn_deterministic = True

4.Fine tuningの実施

Fine tuningは既存のモデルを初期値にして再学習する手法です。
そのため、ベースはResNetを用いて、残りの出力までのネットワークの構造を変更したいといったケースがあります。

例えば、クラス数1000のImageNetの既存モデルからクラス数2のモデルを再学習させたい場合です。
Chainerでは、このようなユースケースにも対応し、簡単に実装できます。
Fine tuningの実装は次のとおりです。
ResNet50Layersのforwardメソッドの出力を"pool5"に指定するとpool5部の出力を獲得し、後の層に繋げられます。

from chainer.links.model.vision.resnet import ResNet50Layers
import chainer
import chainer.links as L
import numpy as np


class ResNetFineTune(chainer.Chain):
    def __init__(self):
        super().__init__()
        with self.init_scope():
            self.resnet = ResNet50Layers()
            self.fc1 = L.Linear(2048, 2)

    def __call__(self, x):
        h = self.resnet.forward(x, layers=["pool5"])["pool5"]
        return self.fc1(h)


x = np.random.rand(1, 3, 224, 224).astype(np.float32)
model = ResNetFineTune()
print(model(x))

5. 実行環境の確認

実行環境の確認は重要です。デバッグ時には、CUDA/CuDNNのバージョンを確認することがあります。
例えば、CUDA/CUDNNのパスを環境変数を設定しています。
しかし、問題が発生したときにどのバージョンがライブラリ内で使われているのかを確認したいです。

> import chainer
> chainer.print_runtime_info()
Platform: Linux-4.4.0-93-generic-x86_64-with-debian-stretch-sid
Chainer: 5.1.0
NumPy: 1.15.0
CuPy:
  CuPy Version          : 5.1.0
  CUDA Root             : /usr/local/cuda
  CUDA Build Version    : 9010
  CUDA Driver Version   : 10000
  CUDA Runtime Version  : 9010
  cuDNN Build Version   : 7102
  cuDNN Version         : 7102
  NCCL Build Version    : 2115
iDeep: Not Available

まとめ

Chainerのトリックを紹介しましたがいかがでしょうか?
デバッグや再現性確認など、仕事でもすぐに使えるものが中心です。
もし、このようなことに苦心しているようでしたらぜひ使ってみてください。

では、メリークリスマス!

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

ZabbixのデータをElasticsearch+Kibanaで可視化してみた

こんにちは。インフラよりのエンジニアtoshikiです。
本記事はZabbix Advent Calendar 2018の23日目です。

はじめに

2018年10月にZabbix 4.0がリリースされました。
試してみたいことはいろいろとありますが、今回はZabbix 4.0でHistoryデータを、時系列データの扱いが得意なElasticsearchに保存することを試してみました。

バックエンドとしてElasticsearchを利用可能となれば、大量データでもスケールするElasticsearchのメリットを活かすことができそうです。
そして、Kibanaを利用して表現力があるグラフを簡単に作れるかもしれません(?)

なお、この機能は3.4から組み込まれていますが、

Elasticsearch support is experimental!  (3 Elasticsearch setup [Zabbix Documentation 4.0])

とのことで、現在の4.0でも実験中の機能になります。

概要

内容としては、以下の流れで説明します。

  1. Zabbixの設定
  2. Elasticsearchのマッピング定義
  3. Elasticsearchにデータ投入されたことを確認
  4. まとめ

構成として、以下に示す3台のサーバ(CentOS 7.6)を使用しました。
Zabbix, Elasticsearch(データストア), Kibana(可視化サーバ)はそれぞれ各サーバにインストールしてあります。
(Elasticsearch、Kibanaのインストールは各サイト(ElasticsearchKibana)を参照ください。)
また、Zabbix agentも各サーバにインストールしています。

種類 Hostname IP Address
Zabbix Server(MariaDB) zabbix 172.16.10.141/24
Elasticsearch elastic01 172.16.10.151/24
Kibana kibana01 172.16.10.150/24

f:id:acro-engineer:20181223214900p:plain:w500
ノード構成

Zabbixの設定

ZabbixのHistoryデータがElasticsearchに保存されるようにします。
設定については以下のページを参照しました。
https://www.zabbix.com/documentation/4.0/manual/appendix/install/elastic_search_setup

具体的には/etc/zabbix/zabbix_server.confに以下を設定します。

### Option: HistoryStorageURL
HistoryStorageURL=http://172.16.10.151:9200

### Option: HistoryStorageTypes
HistoryStorageTypes=uint,dbl,str,log,text

次にZabbixのWeb画面設定として、/etc/zabbix/web/zabbix.conf.phpファイルに以下を記述します。

// Elasticsearch url (can be string if same url is used for all types).
$HISTORY['url']   = [
        'uint' => 'http://172.16.10.151:9200',
        'dbl' => 'http://172.16.10.151:9200',
        'str' => 'http://172.16.10.151:9200',
        'log' => 'http://172.16.10.151:9200',
        'text' => 'http://172.16.10.151:9200'
];
// Value types stored in Elasticsearch.
$HISTORY['types'] = ['uint', 'dbl', 'str', 'log', 'text'];

Elasticsearchのマッピング定義

Elasticsearchにはマッピング定義(データベースのスキーマ定義に相当)を行います。

マッピング定義の内容を確認するため、以下からソースコードを取得します。
https://www.zabbix.com/download_sources

取得したソースコードのファイル(zabbix-4.0.3.tar.gz)を展開すると以下にマッピング定義が書かれたファイルがあります。
zabbix-4.0.3/database/elasticsearch/elasticsearch.map

こちらのマッピング定義の内容を参照し、KibanaのDev Toolsを利用してマッピング定義を行いました。
(以下を実行しています。)

PUT uint
{"settings":{"index":{"number_of_replicas":0,"number_of_shards":1}},"mappings":{"values":{"properties":{"itemid":{"type":"long"},"clock":{"format":"epoch_second","type":"date"},"value":{"type":"long"}}}}}

PUT dbl
{"settings":{"index":{"number_of_replicas":0,"number_of_shards":1}},"mappings":{"values":{"properties":{"itemid":{"type":"long"},"clock":{"format":"epoch_second","type":"date"},"value":{"type":"double"}}}}}

PUT str
{"settings":{"index":{"number_of_replicas":0,"number_of_shards":1}},"mappings":{"values":{"properties":{"itemid":{"type":"long"},"clock":{"format":"epoch_second","type":"date"},"value":{"fields":{"analyzed":{"index":true,"type":"text","analyzer":"standard"}},"index":false,"type":"text"}}}}}

PUT text
{"settings":{"index":{"number_of_replicas":0,"number_of_shards":1}},"mappings":{"values":{"properties":{"itemid":{"type":"long"},"clock":{"format":"epoch_second","type":"date"},"value":{"fields":{"analyzed":{"index":true,"type":"text","analyzer":"standard"}},"index":false,"type":"text"}}}}}

PUT log
{"settings":{"index":{"number_of_replicas":0,"number_of_shards":1}},"mappings":{"values":{"properties":{"itemid":{"type":"long"},"clock":{"format":"epoch_second","type":"date"},"value":{"fields":{"analyzed":{"index":true,"type":"text","analyzer":"standard"}},"index":false,"type":"text"}}}}}

Elasticsearchにデータ投入されたことを確認

上記の設定後、Zabbixを再起動し、各サーバのZabbix agentをHost登録しました。
しばらくしたのち確認すると、Zabbix側では、CPUのグラフが以下のように参照できます。

f:id:acro-engineer:20181223184146p:plain:w740
CPUグラフ

ダッシュボードは以下のようにしてみました。

f:id:acro-engineer:20181223184242p:plain:w740
Zabbixダッシュボード
Zabbix 4.0でダッシュボードがよりカッコよくなりましたよね。

また、Elasticsearch側でも各Index(uint,dbl,str,log)にデータ(ドキュメント)が入っていることをElasticsearchのcat APIsを利用して確認できます。(#Elasticsearch の cat APIs にまじめに入門する話 - Taste of Tech Topics
※textのIndexは対応する監視アイテムがなかったため、docs.countが0になっています。

f:id:acro-engineer:20181223184208p:plain:w760
ElasticsearchのIndex


Kibanaは簡単にグラフを作成できるため、Zabbixのデータ保存数等をグラフ化してみました。

f:id:acro-engineer:20181223184304p:plain:w740
Zabbixデータのカウント

KIbanaはグラフを簡単に作成できますが、当然ながらZabbixのデータはKibanaでグラフを作成するためのデータ構造にはなっていません。
たとえば、上記グラフでは"dbl"インデックスに保存されたNumeric (float)型のデータ件数をカウントするぐらいはできますが、各データ(ドキュメント)にはホスト情報はなく、アイテムIDが格納されいます。
このため、現状では各ホスト毎や、意図した監視アイテムを簡単に選択してグラフ化するのは難しいですね^^;

まとめ

experimental扱いの機能ですが、ZabbixのバックエンドとしてElasticsearchを利用する設定について試してみました。
対象としてはHistoryデータだけですが、Elasticsearchにデータが保存され、Zabbixの画面でグラフを参照できることを確認できました。
いずれ監視対象が多く、大量データを保持するようなケースがあった場合に、Elasticsearchが一つの選択肢になるかもしれませんね。

追加で、上記で試したのは、各データタイプごとに1つのIndexを作成していますが、Zabbix 4.0では日付でIndexを分ける対応が追加されています。(HistoryStorageDateIndexパラメータを有効に設定)

また、Elasticsearchは、ZabbixによるHousekeeperの対象外となっています。
運用で利用するには、まだ機能的な対応が必要ではありますが、Zabbixの進化を期待している私としては、今後どのように開発が進むのか楽しみなところです。

それでは。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
顧客のビジネスをインフラから加速するエンジニア募集! - Acroquest Technology株式会社のインフラエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

キーフレーズを自動推定するPositionRankの収束性について解説

こんにちは。
学生時代に信号処理で使っていた数学の知識を生かして、機械学習関連の仕事をしている3年目の@maron8676です。

本記事はAdvent Calendar 機械学習の数理の21日目の記事となります。

0. はじめに

本記事では、文書からキーフレーズを抽出するアルゴリズムであるPositionRankの収束性について解説します。
原論文[1]には収束について書かれていませんが、アルゴリズムを使うにあたり収束性があるかどうかは気になるところだと思います。
機械学習では初期点によって結果が変わるなんてことはよくある話ですよね
そこで、今回はPositionRankの収束性について関数解析の視点から検証してみます。
結果として、PositionRankのアルゴリズムが作る点列は、任意の初期点に対し唯一つの点に収束するという、とてもよい性質を持っていることが分かります。

1. PositionRankについて

1.1. 目的

PositionRankは文書からキーフレーズ(文書のトピックを表すフレーズ)を抽出するためのアルゴリズムです。
キーフレーズが文書から自動で得られれば、文書を読まなくてもトピックが分かるため便利です。
また、自動で得られたキーフレーズをクラスタリングや、レコメンデーションへ応用することも考えられます。

1.2. 特徴

PositionRankはPageRankから影響を受けているアルゴリズムです。PageRankのページリンクに相当するものが、単語の共起情報となっています。
つまり、重要な単語と共起する単語は重要な単語であるという考えでキーフレーズの抽出を行っていると言えます。
PositionRankでは、単語の共起情報に加え、単語の出現位置を重要度に反映させることによって、共起情報だけを使った場合より精度を高めています。
それでは、PositionRankではどのようにしてキーフレーズを求めているのか見ていきましょう。

1.3. アルゴリズム

PositionRankでは、図1のような流れで各単語の重要度を算出し、高い重要度の単語を使ってキーフレーズを構築します。
単語の重要度が出てしまえば、高い重要度の単語を使ってキーフレーズを構成できます。
そのため以下では、各単語の重要度を導出するアルゴリズムについて注目していきましょう。

f:id:acro-engineer:20181217200127p:plain
図1. PositionRankで単語の重要度を導出するアルゴリズムの流れ

1.3.1. 文書から単語の共起情報を表すグラフを作る

ある単語とある単語が同じ場所で使われることを共起と言います。この共起情報を使って文書からグラフを作ることができます。
例えば、2つ隣までの単語は共起していることにすると、「PositionRankは文書からキーフレーズを抽出するためのアルゴリズムです」という文から
以下のグラフを作れます。

f:id:acro-engineer:20181217194955p:plain
図2. 例文から構成されるグラフ

図2では辺の重みが全て1のため省略しますが、辺の重みは共起回数となります。
実際にはいくつかの文があるため、さらに大きなグラフとなります。

1.3.2. グラフから各単語の重要度を計算する

作ったグラフの隣接行列M \in \mathbb{R}^{|V| \times |V|}と、単語の出現位置から構成したベクトルp \in \mathbb{R}^{|V|}を使い、初期点x_0 \in \mathbb{R}^{|V|}|V|はグラフの頂点数=単語の種類)から以下の計算を繰り返し行うことで各単語の重要度を求めます。
x_{n+1} = \alpha \widetilde{M} x_n + (1-\alpha)p \qquad (0 < \alpha <1)
\widetilde{M}はグラフの隣接行列Mの各行を和が1になるようにl_1ノルムで正規化した行列であり、
p=\Bigl [ \frac{p_1}{p_1+p_2+ \ldots +p_{|V|}},\frac{p_2}{p_1+p_2+ \ldots +p_{|V|}}, \ldots ,\frac{p_{|V|}}{p_1+p_2+ \ldots +p_{|V|}} \Bigr ]^T.
ここで、p_iは単語の出現位置から計算される値です。例えば、i番目の単語が文書の5番目と7番目に出現した場合はp_i=\frac{1}{5}+\frac{1}{7}=\frac{12}{35}となります。

2. アルゴリズムの収束性

それでは、1.3.のアルゴリズムによって作られる点列が唯一つの点に収束することを確認していきましょう。
図3の流れで確認を進めていきます。

f:id:acro-engineer:20181218073153p:plain
図3. 収束を証明する流れ

2.1. 行列\widetilde{M}による線形写像は非拡大写像である

行列\widetilde{M}の各行がl_1ノルムで正規化されていることを使って、非拡大写像であることを示します。
ここでの非拡大写像とは、完備なノルム空間における任意の  x, y \in \mathbb{R}^{|V|} に対し、以下が成り立つ写像A : \mathbb{R}^{|V|} \rightarrow \mathbb{R}^{|V|}のことです。
\|x-y\| \geq \|Ax-Ay\|
\|\cdot\|はノルムです。これ以降はノルムをl_{\infty}ノルム*1として進めていきます。最終的な収束の結果については、ノルムの等価性*2を使うと\mathbb{R}^{|V|}上の全てのノルムについて成り立つことが確認できます。
\widetilde{M}が線形写像であることに注意すると、
\begin{align}\|\widetilde{M}x-\widetilde{M}y\|_{\infty}&=\|\widetilde{M}(x-y)\|_{\infty}\\
&\leq\|\widetilde{M}\|_T\|x-y\|_{\infty}\end{align}
となります。\|\cdot\|_T作用素ノルム
\|A\|_T:=\max_{\|x\|_{\infty}=1}\|Ax\|_{\infty}
です。行列\widetilde{M}の各行はl_1ノルムで正規化されているため、\|\widetilde{M}\|_T = 1となります*3。したがって、行列\widetilde{M}による線形写像は非拡大写像です。

2.2. 実数 0 < \alpha < 1 によるpとの凸結合は縮小写像である

単語の出現位置から構成したベクトルpとの凸結合が縮小写像であることを示します。
ここでの凸結合とは、実ベクトル空間の任意の点x,y \in \mathbb{R}^{|V|}と実数0 < \alpha < 1から得られる
\alpha x + (1-\alpha)y
のことを言います。また、写像fが縮小写像であるとは、完備なノルム空間における任意の点x,y \in \mathbb{R}^{|V|}に対しあるk \in [0,1)が存在して、
\|f(x)-f(y)\|\leq k\|x-y\|
が成り立つことです。
fpとの凸結合
f(x)= \alpha x+(1-\alpha)p \quad (0 < \alpha < 1)
とすると、
\begin{align}\|f(x)-f(y)\|_{\infty}&=\|\alpha x+(1-a)p-\alpha y-(1-\alpha)p\|_{\infty}\\
&=\|\alpha x-\alpha y\|_{\infty}\\
&\leq|\alpha|\|x-y\|_{\infty}\end{align}
となるため縮小写像となります。

2.3. バナッハの不動点定理[2]を適用する

バナッハの不動点定理[2]は以下が成り立つことを示しています。
Tを完備距離空間における縮小写像とすると、Tは唯一つの不動点を持つ
点列x_n=T(x_{n-1})は唯一つの不動点に収束する。
2.1.と2.2.より、PositionRankの1イテレーション
g(x_{n}) = \alpha \widetilde{M} x_n + (1-\alpha)p \qquad (0 < \alpha <1)
は縮小写像であるため、バナッハの不動点定理[2]より唯一つの不動点が存在し、イテレーションを繰り返すことでその不動点に収束します。
   

3. アルゴリズムの収束速度

バナッハの不動点定理[2]により、収束速度も以下のように得ることができます。
\|x^* - x_k\| \leq \frac{\alpha^k}{1-\alpha}\|x_1 - x_0\|
ここで、x^*不動点k \in \mathbb{N}イテレーション回数であり、\alphaは2.2.の凸結合に登場する\alphaです。
実際の例として、距離がl_{\infty}ノルムで\alpha = 0.85*4とした場合を見てみましょう。
ランダムに決める初期値としては、原論文[1]で使われている、全ての成分が\frac{1}{|V|}のベクトルを使うことにしましょう。すると、 \|x_1 - x_0\|_{\infty} < 1となるため、100回イテレーションを回して得られるベクトルx_{100}
\begin{align}\|x^* - x_{100}\|_{\infty} &\leq \frac{\alpha^{100}}{1-\alpha}\|x_1 - x_0\|_{\infty}\\
&=\frac{0.85^{100}}{0.15}\|x_1 - x_0\|_{\infty}\\
&< 5.8 \times 10^{-7}\end{align}
を満たします。100回のイテレーションで十分収束していると言えますね。

4. まとめ

キーフレーズを抽出するアルゴリズムであるPositionRankの1イテレーションが縮小写像であることを示して、唯一つの点に収束することと収束の速度を確認しました。任意の初期点から始めて収束する+指数オーダで収束速度もいいという結果で、安心して使えます。実用上いい結果が出ることも大事なのですが、今回のように収束を検証できることも重要なことなので、何となく使っていたアルゴリズムを見直してみるのもよいのではないでしょうか。

5. 参考文献

[1] Corina Florescu and Cornelia Caragea, PositionRank: An Unsupervised Approach to Keyphrase Extraction from Scholarly Documents
[2] S. Banach, Sur les operations dans les ensembles abstraits et leur application aux equations integrales, Fund. Math. 3(1922), 133–181
[3] 山田 功, 工学のための関数解析, 数理工学社
[4] Sergey Brin and Lawrence Page, The anatomy of a large-scale hypertextual Web search engine

*1 ベクトルx \in \mathbb{R}^{|V|}l_{\infty}ノルムは\|x\|_{\infty} := \max_{1 \leq i \leq |V|}|x_i|です。

*2 ベクトル空間Xに定義されたノルム\|\cdot\|_Aとノルム\|\cdot\|_Bが等価であるとは、ある実数M_2 \geq M_1 > 0が存在して、すべてのx \in Xに対し、M_1 \|x\|_A \leq \|x\|_B \leq M_2 \|x\|_A
が成り立つことを言います。有限次元ベクトル空間に定義可能なノルムはすべて等価であることが知られています([3]を参照されたい)。

*3 証明は省略しますが([3]を参照されたい)、行列とベクトルの積を成分で展開し、
1. (積の絶対値)\leq(絶対値の積)から上界
2. 単位ベクトルとの積から下界
を作って挟み込むことで、l_{\infty}ノルムに対応する行列A \in \mathbb{R}^{|V|}作用素ノルムは
\|A\|_T:=\max_{1 \leq j \leq |V|} \Sigma_{k=1}^{|V|}|a_{jk}|
となることを示せます。

*4 PageRankのdamping factor[4]にならって0.85を設定することが多いようです。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Masterと働きたい尖ったエンジニアWanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

Elastic Stackで簡単!Dockerコンテナ監視ダッシュボード作成

こんにちは、CI/CDツールなどを活用し、DevOps推進活動などに携わっている横山です。
本記事は、Docker Advent Calendar 2018 - Qiita20日目です。

はじめに

Elastic Stackを使うと、簡単にDockerコンテナの監視ダッシュボードが作成できるので、今回はその紹介をしたいと思います。

きっかけとしては、「開発環境で立ち上げている複数コンテナの問題調査を楽にしたい」というのがあります。最近、開発環境に複数のコンテナを立ち上げて開発メンバーに提供していますが、開発メンバーから「重たいので環境を確認してほしい」といった声が上がってきます。その際、どのサーバのどのコンテナに問題が発生しているのか確認したいですが、その都度サーバに入って、docker statsなどのコマンドで確認するのはやや面倒です。

そこで、コンテナの監視ダッシュボードを作って、一か所で確認できるようにしようと考え、Elastic Stackを使うことにしました。

今回の記事では、以下のダッシュボードを作ります。
f:id:acro-engineer:20181220080124p:plain:w700

概要

内容としては、以下の流れで説明します。

  1. 監視対象のコンテナを起動
  2. Elasticsearch、Kibanaをコンテナで起動
  3. Metricbeatのインストール、設定
  4. Kibanaダッシュボードの確認

構成は以下のようになります。
f:id:acro-engineer:20181220103953p:plain:w500

環境、バージョン情報

※今回は、全て同一サーバ上で動かします。

OS CentOS 7.5
Elastic Stack 6.5.3
Docker 18.09
Docker Compose 1.23.1

Docker、Docker Composeはインストールしている前提とします。
Dockerのインストール方法については、
Docker入門(第二回)~Dockerセットアップ、コンテナ起動~ | さくらのナレッジ
を参照してください。

手順

監視対象コンテナを起動

今回は、監視対象として、RedmineのDockerコンテナを起動します。

任意のディレクトリにdocker-compose_redmine.ymlを作成します。

version: '3.1'

services:

  redmine:
    image: redmine
    restart: always
    ports:
      - 3000:3000
    environment:
      REDMINE_DB_MYSQL: db
      REDMINE_DB_PASSWORD: example

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: redmine

Redmineのコンテナを起動します。

$ docker-compose -f docker-compose_redmine.yml up -d
Creating network "redmine_default" with the default driver
Creating redmine_db_1_79a9b94cfd42      ... done
Creating redmine_redmine_1_195c7e2fd8c7 ... done

$ docker-compose -f docker-compose_redmine.yml ps
             Name                           Command               State           Ports         
------------------------------------------------------------------------------------------------
redmine_db_1_36b65927def4        docker-entrypoint.sh mysqld      Up      3306/tcp, 33060/tcp   
redmine_redmine_1_870966ab277a   /docker-entrypoint.sh rail ...   Up      0.0.0.0:3000->3000/tcp
$

Elasticsearch、Kibanaのコンテナを起動

Elasticsearch、Kibanaについても公式のDockerイメージを利用して起動します。

任意のディレクトリに以下のようなdocker-compose_elk.ymlを作成します。

version: '2.2'

services:

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.5.3
    container_name: elasticsearch
    environment:
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9200:9200
    networks:
      - esnet

  kibana:
    image: docker.elastic.co/kibana/kibana:6.5.3
    ports:
      - 5601:5601
    networks:
      - esnet

networks:
  esnet:

Elasticsearch起動のために「vm.max_map_count」を確認し、設定します。
まず、現状の値を確認します。

$ sudo sysctl -a | grep vm.max_map_count
・・・
vm.max_map_count = 65530

値が「262144」になっていない場合は、以下のコマンドで変更します。
(永続化する場合は、/etc/sysctl.confに設定します。)

$ sysctl -w vm.max_map_count=262144
vm.max_map_count = 262144

Elasticsearch、Kibanaのコンテナを起動します。

$ docker-compose -f docker-compose_elk.yml up -d
Creating network "elk_esnet" with the default driver
Creating elk_kibana_1_730c2b554976 ... done
Creating elasticsearch             ... done
 
$ docker-compose -f docker-compose_elk.yml ps
          Name                         Command               State                Ports              
-----------------------------------------------------------------------------------------------------
elasticsearch               /usr/local/bin/docker-entr ...   Up      0.0.0.0:9200->9200/tcp, 9300/tcp
elk_kibana_1_d87486e7e64b   /usr/local/bin/kibana-docker     Up      0.0.0.0:5601->5601/tcp          
$ 

Metricbeatのインストール、設定

監視対象のコンテナ情報を取得するために、Metricbeatをインストールします。

$ curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-6.5.3-x86_64.rpm
$ sudo rpm -vi metricbeat-6.5.3-x86_64.rpm

補足になりますが、Elasticsearch、Kibanaと、
Metricbeatを別サーバで動かすときは、以下の接続先(localhostの部分)の設定を変更します。

$ sudo vi /etc/metricbeat/metricbeat.yml

setup.kibana:
  host: "localhost:5601"

output.elasticsearch:
  hosts: ["localhost:9200"]

MetricbeatのDockerコンテナ用のモジュールを有効にします。

$ sudo metricbeat modules enable docker
Enabled docker
$ sudo vi /etc/metricbeat/modules.d/docker.yml

コメントアウトを外し、以下のように設定します。
(記述していない部分はデフォルトのままです。)
- module: docker
  metricsets:
    - container
    - cpu
    - diskio
    - healthcheck
    - info
    - memory
    - network

ElasticsearchのIndex Templateを適用します。

$ sudo metricbeat setup --template -E output.logstash.enabled=false -E 'output.elasticsearch.hosts=["localhost:9200"]'
Loaded index template

Kibanaのダッシュボード定義を作成します。
(テンプレートがあるのはうれしいですね。)

$ sudo metricbeat setup --dashboards
Loading dashboards (Kibana must be running and reachable)
Loaded dashboards

Metricbeatを起動し、確認します。

$ sudo systemctl start metricbeat
$ sudo systemctl status metricbeat

これで、一通りの設定が完了です。
それでは、Kibanaの画面で確認してみましょう。

Kibanaダッシュボード画面

ブラウザで以下のURLにアクセスし、Kibanaの画面を開きます。
 http://<KibanaサーバのIPアドレス>:5601

Kibanaのトップページ左のメニューから「Dashboard」をクリックします。
f:id:acro-engineer:20181220075940p:plain:w750

ダッシュボード一覧が表示されるので「[Metricbeat Docker] Overview」をクリックします。
f:id:acro-engineer:20181220080026p:plain:w500

以下のようなダッシュボードが表示されます。
f:id:acro-engineer:20181220080042p:plain:w750

監視対象用に起動しているRedmineコンテナ(redmine_redmine_1_dda7715e7770)やMySQLコンテナ(redmine_db_1_e54eba45c028)の監視ができています。
また、今回はElasticsearch、Kibanaもコンテナで起動しているので、それらも合わせて監視されています。

上図の赤丸部分で、MySQLコンテナのメモリ使用量が増えていることがパッと見てわかります。

このように、複数のコンテナが動いている環境で問題調査をする際に、このようなダッシュボードで切り分けが楽になりました。


余談ですが、ダッシュボードの一覧で、「[Metricbeat System] Host overview」を選択すると、ホスト側の状態も確認できます。
f:id:acro-engineer:20181220080057p:plain

まとめ

今回は、Elasticsearch、 Kibana、MetricbeatでDockerコンテナの監視を行ってみました。
MetricbeatのモジュールやKibanaダッシュボードのテンプレートなどが用意されているので、簡単に監視ダッシュボードが作れるのは良いですね。
ダッシュボードのカスタマイズなどもして、より使い勝手の良いダッシュボードにもしてみたいと思います。

それでは。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
顧客のビジネスをインフラから加速するエンジニア募集! - Acroquest Technology株式会社のインフラエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com

JShellでフォルダ出力するワンライナーを書いてみた。

概要

こんにちは、最近JShellに嵌っているuedaです。
この記事はワンライナー自慢大会 Advent Calendar 2018 - Qiitaの19日目です。
JavaのJShellを使ってテキストに書かれたとおりの構成のフォルダを出力する
ワンライナーを紹介します。

昔はJavaワンライナーなんて考えもしませんでしたが、
Java9から使えるようになったJShell(REPL)で対話的に実行する事で、
ワンライナーっぽく書ける様になりました。
中々使う機会が無いので、勉強がてらワンライナーを作成しました。

f:id:acro-engineer:20181218044827j:plain:w200
duke

環境

Java: openjdk version "11.0.1" 2018-10-16
OS: Windows10 Home

※本稿のワンライナーはJava11のAPIを使用しているため、Java10以前では動きません。

JShellを使ったJavaコマンド実行への道

Java11のダウンロード

JDKはいくつか種類がありますが、今回はAdoptOpenJDKを使いました。
adoptopenjdk.net

パスを通す

展開したAdoptOpenJDKのbinにパスを通し、「JShell」をコマンドプロンプトから実行できるようにします。

コマンドプロンプトからJShellを経由してのJava実行

> echo System.out.plintln("Hello.") | JShell -

このように「echo」+「生のJavaコード」+パイプ~JShell~ハイフンでJavaコードの単発実行ができます。
クラスやmainメソッドを書かなくて済む事を考えると、大分楽です。

注意点として、コマンドプロンプト実行ではechoコマンドが「>」を評価してしまうため、「^」(キャレット)でエスケープする必要があります。
例えば以下のような記述になります。

例 (i -> i * 10) → (i -^^^> i * 10)

本題(フォルダ出力ワンライナー)

さて、本題のワンライナーについて。
大量のディレクトリを作んなきゃ、て事はたまにありますよね。
そんな時、さくっと生成したいと考え、
テキストに書いたとおりにフォルダを出力するワンライナーを書きました。

input.txt

Excelからぺっと貼り付けたタブインデント付のテキストです。
このテキストの構成どおりのフォルダ作成を考えます。

test
	test1
		1-1
		1-2
	test2
		1-1
		1-2
		2-1
		3-1
			images
			procedure
		3-2
			images
			procedure
		3-3
			images
			procedure

本稿の主役のワンライナー

上記input.txtと同じフォルダで下記ワンライナーを実行すると、テキストに記載された構成通りのフォルダが生成されます。

>echo Stream.of(new LinkedHashMap^^^<Integer, String^^^>()).forEach(map -^^^> {try { Files.readAllLines(Path.of("input.txt")).stream().peek(k -^^^> map.put(k.length() - k.stripLeading().length(), k.strip())).map(s -^^^> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).peek(System.out::println).forEach(r -^^^> {try {Files.createDirectory(Path.of(r));} catch (IOException e) {throw new UncheckedIOException(e);}});} catch (IOException e) {throw new UncheckedIOException(e);}}); | JShell -

解説

ワンライナーのままだと読みづらいので、展開してみます。

Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> {
	try {
		Files.readAllLines(Path.of("input.txt")).stream()
				.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
				.map(s -> s.length() - s.stripLeading().length() == 0 ? s
						: String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length())
								.collect(Collectors.toList())) + "/" + s.strip())
				.peek(System.out::println)
				.forEach(r -> {
					try {
						Files.createDirectory(Path.of(r));
					} catch (IOException e) {
						throw new UncheckedIOException(e);
					}
				});
	} catch (IOException e) {
		throw new UncheckedIOException(e);
	}
});
最初のStreamは、処理中で使用するLinkedHashMap生成のためだけに行います。
Stream.of(new LinkedHashMap<Integer, String>()).forEach(map -> 以降実際の出力処理
ファイルを読み込み、ファイル1行ごとの文字列に対しストリーム処理を行います。
Files.readAllLines(Path.of("input.txt")).stream()
peekはストリームに影響を与えない処理を行うため、ここでMapにファイル内容を格納しています。

 Mapはキーがタブインデントの数、バリューがフォルダ名になるよう加工しています。

.peek(k -> map.put(k.length() - k.stripLeading().length(), k.strip()))
相対パスの生成を行っています。

 タブ数が0ならそのまま、0以外なら全親フォルダのパス+自分のフォルダ名がストリームに流れます。

.map(s -> s.length() - s.stripLeading().length() == 0 ? s : String.join("/", map.values().stream().limit(s.length() - s.stripLeading().length()).collect(Collectors.toList())) + "/" + s.strip()).
生成された相対パスのDEBUG出力です。
.peek(System.out::println)
生成された相対パスに対し順次フォルダ生成を行います。
forEach(r -> {try {Files.createDirectory(Path.of(r));}


書いてみて、今回の処理は流れをさかのぼる処理があったことから、
最初のmap生成など強引な処理になっているところがあります。
この辺りワンライナーに向いていなかったのではないかと後になって気がつきました。

ちなみに

Streamを使わずに、展開して記述すると以下のようになります。

	var map = new LinkedHashMap<Integer, String>();
	var resultPathList = new ArrayList<String>();
	List<String> lines = null;
	lines = Files.readAllLines(Path.of("input.txt"));

	for (String line : lines) {
		String pathName = "";
		int tabCount = line.length() - line.stripLeading().length();
		line = line.strip();
		map.put(tabCount, line);
		if (tabCount == 0) {
			pathName = line;
		} else {
			for (int index = 0; index < tabCount; index++) {
				pathName += map.get(index) + "/";
			}
			pathName += line;
		}
		resultPathList.add(pathName);
	}

	for (String resultPath : resultPathList) {
		System.out.println(resultPath);
		Files.createDirectory(Path.of(resultPath));
	}

24Lineぐらいありますね。是に比べるとStreamを使ったコードはスッキリしています。
こちらのほうが読みやすいですが。。

まとめ

以上、JShellで書いたワンライナー、いかがでしたでしょうか。
Javaはあまり向いていないとは思いますが、
ストリームを書く練習にはちょうどよいのではないかと思います。

ではでは。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

世界初のElastic認定エンジニアと一緒に働きたい人Wanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップの求人 - Wantedlywww.wantedly.com

Re:VIEW 3.0でレイアウトをカスタマイズしたい!

こんにちは、@snuffkinです。
このエントリは技術同人誌 その2 Advent Calendar 2018の18日目の記事です。

みなさん、技術同人誌、書いてますか~
私は技術書典5で初めて技術同人誌を作成しました。
acro-engineer.hatenablog.com
acro-engineer.hatenablog.com


このとき初めてRe:VIEWに触れたのですが、とても便利ですね。
Markdownライクな独自フォーマットで記述した原稿から、書籍の形式のPDFを簡単に生成できます。

ただ、技術書典5では原稿を書くためにパワーの大半を使ってしまい、書籍のレイアウトにはあまり時間を割けませんでした。
その後、Re:VIEWが3.0にバージョンアップしたのを機に、レイアウトをカスタマイズするために必要な知識を学びました。
この記事では、Re:VIEWでPDFを生成したことがあり、TeXの知識が多少ある方に向けて、Re:VIEWのレイアウト修正方法についてご紹介します。

Re:VIEWの情報源

Re:VIEWのサイトはこちらになります。
Re:VIEW - Digital Publishing System for Books and eBooks(https://reviewml.org/ja/)

カスタマイズして利用したい方は、こちらのナレッジベースが参考になります。
Re:VIEW ナレッジベース — Re:VIEW knowledge ドキュメント(https://review-knowledge-ja.readthedocs.io/ja/latest/)

デフォルト設定でPDFを出力

PDFを生成するには、config.ymlのtexdocumentclassにmedia=ebookを指定し、

texdocumentclass: ["review-jsbook", "media=ebook,paper=a5"]

次のコマンドを実行します。

rake pdf

生成されたPDFは次のようなレイアウトになっています。
f:id:acro-engineer:20181216132648p:plain

このレイアウトには気になる点があったため、赤枠で囲いました。
デフォルトの設定では、各ページのヘッダに章名と節名が表示されています。
私の感覚としては、

  • 偶数ページの左側に章名
  • 奇数ページの右側に章名

とし、赤枠の部分には何も出力されないようにしたいです。

レイアウトを変更

そこで、レイアウトを変更してみましょう。
Re:VIEWのレイアウト情報はstyディレクトリにTeXで記載されています。
そのため、レイアウトをカスタマイズするには、TeXの知識が必要になります。

ページヘッダはsty/review-style.styの次の部分になります。

\RequirePackage{fancyhdr}
\pagestyle{fancy}
\lhead{\gtfamily\sffamily\bfseries\upshape \leftmark}
\chead{}
\rhead{\gtfamily\sffamily\bfseries\upshape \rightmark}

TeXのfancyhdrパッケージを使っています。
lhead、rheadの仕様は

  • \lhead[偶数ページのヘッダの左側]{奇数ページのヘッダの左側}
  • \rhead[偶数ページのヘッダの右側]{奇数ページのヘッダの右側}

であり、[]を省略すると偶数ページ・奇数ページ共に同じヘッダになります。

ヘッダのレイアウトを偶数ページと奇数ページで分ければいいんですね。

Re:VIEWでスタイルを修正したい場合は、sty/review-custom.styに記載します。
review-custom.styは他のstyより後に読み込まれるため、ここに記載した内容でデフォルト設定を上書きすることができます。

\lhead[\gtfamily\sffamily\bfseries\upshape \leftmark]{}
\rhead[]{\gtfamily\sffamily\bfseries\upshape \rightmark}

さて、これでrake pdfしてみると、、、
期待通りのヘッダになりました!
f:id:acro-engineer:20181216132704p:plain

このように、TeXの知識があればレイアウトを変更することができます。
ちょっとレイアウトを変更したいのであれば、このような流れで対応できますね。

おまけ: @<m>でTeXを使うときに「}」はエスケープしなくて良いです!

TeXで「\sum_{x=1}^n x」と出力するとき、次のように書きます。

\sum_{x=1}^n x

Re:VIEWにインライン命令でTeX記法を使うときは「@<m>」を使いますが、多くのサイトで

@<m>{\sum_{x=1\}^n x}

と書くよう説明されています。
「}」をエスケープする必要があるため、「x=1」の後ろの「}」を「\}」と書く必要があります。
TeXは大量の「}」を使うので、いちいちエスケープするのは非常につらいです。

これは最近のRe:VIEWでは改善されていて、「@<m>」の開始と終了に「$」を指定できるようになっています。
「$」を使えば「}」はエスケープしなくて良いです!

@<m>$\sum_{x=1}^n x$

これで、Re:VIEWの原稿にTeXを書きやすくなりますね。

最後に

Re:VIEWはとても素晴らしいツールで、手軽に技術書を書くことができます。
是非皆さんも技術書を書いてみませんか。

では、次回の技術書典で(?)お会いしましょう~

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
モノリシックなアプリケーションをマイクロサービス化したいエンジニア募集! - Acroquest Technology株式会社のWeb エンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com