RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

Android アプリのプロキシ設定について

はじめに

こんにちは、まもなくサッカーのヨーロッパ主要リーグが開幕するということでワクワクしながら過ごしている APP/UX チームの杉山です。
私は特定のチームのサポではありませんが、プレシーズンマッチで絶好調のアーセナルがリーグ戦でどこまでいけるか楽しみにしています。

今回は、Android アプリのプロキシ設定について書いていきます。

さっそくプロキシ設定

Wi-Fi にプロキシ設定を行う場合

  1. 設定アプリを開く
  2. Wi-Fi 項目を選択する
  3. 利用可能なネットワーク一覧からプロキシを設定したいネットワークの詳細設定を開く
  4. 詳細設定内のプロキシ項目を選択する
  5. 「手動」を選択し、ホスト名、ポートなどの必要情報を入力する

*設定アプリ内の表示内容は端末により異なる為、名称などが違う場合がございます。

アプリ内でプロキシ設定を行う場合

利用するライブラリ

  • okhttp3
  • okhttp-digest

実際にコードを書いていく

val builder = OkHttpClient.Builder()

try {
    val proxy = 
        Proxy(
            Proxy.Type.HTTP, 
            InetSocketAddress.createUnresolved([プロキシのホスト名], [プロキシのポート名])
        )
    builder.proxy(proxy)
} catch (e:Exception) {
    Log.e(TAG, [error message])
}

val credentials = 
    Credentials([ユーザーネーム], [パスワード])
val digestAuthenticator = 
    object: DigestAuthenticator(credentials) {}
digestAuthenticator.isProxy = true
builder.proxyAuthenticator(digestAuthenticator)

return builder.addInterceptor {
    val build = 
        it.request()
          .newBuilder()
          .addHeader([name], [value])
          .build()
    return@addInterceptor it.proceed(build)
}.build()

何をやっているか見ていく

プロキシ接続用のエントリを作成し、このクライアントによって作成された接続で使用される HTTP プロキシを設定

val proxy = 
    Proxy(
        Proxy.Type.HTTP, 
        InetSocketAddress.createUnresolved([プロキシのホスト名], [プロキシのポート名])
    )
builder.proxy(proxy)

Authenticator に Credential を設定

val credentials = 
    Credentials([ユーザーネーム], [パスワード])
val digestAuthenticator = 
    object: DigestAuthenticator(credentials) {}

Authenticator にプロキシであるかを設定

digestAuthenticator.isProxy = true

プロキシサーバーからのチャレンジに応答するために使用する Authenticator を設定

builder.proxyAuthenticator(digestAuthenticator)

Interceptor に登録し、OkHttpClient を返す

return builder.addInterceptor {
    val build = 
        it.request()
          .newBuilder()
          .addHeader([name], [value])
          .build()
    return@addInterceptor it.proceed(build)
}.build()

アプリで組み込む際に行うと便利なこと

会社などでアカウントが発行される場合、ユーザーネーム、パスワードが違うと思われるので、アプリ起動時に情報を入力させ保持しておくと良きです。
入力方式はどのようなものでも構いませんが、情報を保持していない時だけ表示されるカスタムダイアログなどにすると実装コストも減ります。

終わりに

リモートワークも増え、このような実装をする機会があるエンジニアの方に有益な情報であれば幸いです。
最後までご覧頂きありがとうございました。

We're hiring!

rarejob-tech.co.jp

TorchScript入門 n番煎じ

こんにちは、EdTechLabの水谷です。

私の所属するEdTechLabで行なっている業務の一つとして、スピーキングテストPROGOSの自動採点を行う機械学習モデルをはじめ、各種機械学習モデルの開発から保守・運用までの一通りを担当しています。

今回は、一般にその中でも障壁が高いと言われる開発からデプロイの間のギャップに焦点を当て、人気のDeep LearningフレームワークであるPyTorchが提供するプロダクション環境向けモデルのTorchScriptについて解説していきたいと思います。

PyTorch

機械学習モデルの開発段階では試行錯誤・デバッグのし易さが実行速度よりも重要なことが多いです。そのため、Pythonのようなインタープリタ型で動的型付けの言語が以下の点で大きな利点があり、機械学習で広く使われています。

  • printやpdbで変数の中身を逐次確認でき、デバッグが容易
  • 少ない記述量で直感的にプログラミングできる

PythonDeep LearningフレームワークであるPyTorch

  • デフォルトではeager execution (define-by-run型)で、Tensorオブジェクトの中身が何かを即時にprint等で確認することができ、デバッグがしやすい
  • Pythonicな哲学で設計されており、またPythonのsyntaxをそのまま使うことができるため、学習コストが少ない上、直感的に記述することができる

などといったメリットがあり、人気のDeep Learningフレームワークとなっています。

一方で、define-by-runでは事前に計算グラフを構築しないためグラフのコンパイルによる処理の最適化が行えず、また動的型付けのためにメモリの最適化も行いづらいため計算量・メモリ消費量にオーバーヘッドがあり、eager modeのPytorchのモデルをプロダクションにデプロイするのは不向きと言えます。

TorchScript

試行錯誤が少なく、かつ推論速度やメモリ削減が要求されるプロダクションには、PyTorch (eager mode) やPythonの持つメリットよりも、静的型付けでコンパイル(define-and-run)型の最適化されたモデルの方が相応しそうです。

そのような特徴を持ったものがTorchScriptで、以下の特徴があります。

  1. 静的型付けでdefine-and-run型なので、計算グラフをコンパイルすることが可能で計算量・メモリの最適化が可能
  2. Pythonのランタイムに依存せず、C++のライブラリであるlibtorchから実行可能でモバイル/エッジデバイスにもデプロイでき、PythonのGlobal Interpreter Lockの制約も受けないため、スレッド並列化が可能

そもそもTorchScriptとは何なのか

TorchScriptという単語は少し乱用され気味で、公式ドキュメントにもさまざまな定義があります。

https://pytorch.org/docs/stable/jit.html#torchscript

TorchScript is a way to create serializable and optimizable models from PyTorch code

https://pytorch.org/docs/stable/jit.html#torchscript-language

TorchScript is a statically typed subset of Python

https://pytorch.org/tutorials/beginner/Intro_to_TorchScript_tutorial.html

TorchScript, an intermediate representation of a PyTorch model


TorchScript, our high-performance deployment runtime

それぞれ、PyTorchから変換する方法、Pythonのサブセット、IR、ランタイムなど、文脈によって異なるものを差しており、混乱の元になりかねないので、これ以降の本投稿では単にTorchScriptという言葉は極力避け、より限定的な単語を使って解説したいと思います。

TorchScript IR

PyTorchではtorch.nnモジュールを使ってモデルを構築します。それぞれのtorch.nnモジュールは内部にパラメータを持ち、テンソルを受け取り、テンソルを返すようなクラスですが、これらはC++の拡張モジュールとして実装されています。

つまり、実際のモデルの数値計算C++レベルで行われており、このPyTorchのコアと呼べるC++の自動微分ライブラリはATenと呼ばれています。

具体的にどのように呼ばれているかをnn.Linearソースコードを例にとって追っていくと、nn.Linearforward中では、torch.nn.funcitonal.linearという関数が呼ばれており、さらにtorch.nn.functional.linearの中身を確認すると、

torch._C._nn.linear

エイリアスとなっています。この定義はaten/src/ATen/native/Linear.cppにあり、実際の計算はC++のレベルで行われていることが分かります。(その後、バックエンド (e.g. cudnn, nnpack, mkldnn) に応じた適切なカーネルが呼ばれます。)

このように、torch.nnで構築したモデルの計算は全てATenで行われるので、計算グラフさえ分かれば理論的にはATenでモデルを構築する事が出来そうです。実際にPyTorchではモデルの計算グラフを表す中間表現(IR: Intermediate Representation)から独自のC++インタープリタを使ってモデルを構築する事ができ、この計算グラフを表す中間表現はTorchScript IRと呼ばれます。TorchScript IRが得られればPythonインタープリタに依存せずにモデルの構築ができ、また静的型付けになっているので効率的にコンパイラ最適化を行う事が出来ます。

TracingとScripting

PytorchのモデルからTorchScript IRを得る方法は以下の2通りがあります。

  1. Tracingを使った方法
  2. Scriptingを使った方法

Tracing

1の方法は明らかな方法で、PyTorchはdefine-by-runなので出力を得るまでグラフは分かりませんが、出力が得られれば、それまでに行われた処理の記録を辿ることで計算グラフを得ることが出来ます。

よって、PyTorchモデルと適当な入力テンソルさえあればTorchScript IRを得る事ができ、この方法はtracingと呼ばれます。

具体的には、torch.jit.trace関数にPytorchモデルインスタンスと入力テンソルを引数として渡すことで、torch.jit.ScriptModule(の子クラス)のモデルに変換できます。

class MyModel(torch.nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.fc1 = torch.nn.Linear(4, 4)
        self.softmax = torch.nn.Softmax(dim=-1)

    def forward(self, x):
        return self.softmax(self.fc1(x))

pytorch_model = MyModel()
dummy_input = torch.rand(1, 4)
script_model = torch.jit.trace(pytorch_model, dummy_input)
print(isinstance(script_model, torch.jit.ScriptModule))  # True

また、変換されたscript_modelpytorch_modelの出力が一致していることのほか、script_modelでもauto-gradが使えていることが確認できます。

print(pytorch_model(dummy_input))  # tensor([[0.2628, 0.3168, 0.2951, 0.1253]], grad_fn=<SoftmaxBackward0>)
print(script_model(dummy_input))  # tensor([[0.2628, 0.3168, 0.2951, 0.1253]], grad_fn=<SoftmaxBackward0>)

TorchScript IRの情報も持っており、.graphプロパティでグラフを見る事ができます。

print(script_model.graph)

Out:

graph(%self.1 : __torch__.___torch_mangle_11.MyModel,
      %x : Float(1, 4, strides=[4, 1], requires_grad=0, device=cpu)):
  %softmax : __torch__.torch.nn.modules.activation.___torch_mangle_10.Softmax = prim::GetAttr[name="softmax"](%self.1)
  %fc1 : __torch__.torch.nn.modules.linear.___torch_mangle_9.Linear = prim::GetAttr[name="fc1"](%self.1)
  %24 : Tensor = prim::CallMethod[name="forward"](%fc1, %x)
  %25 : Tensor = prim::CallMethod[name="forward"](%softmax, %24)
  return (%25)

この方法は、殆ど何も手を加える事なくPyTorchモデルをTorchScript moduleに変換する事ができる一方で、特定のInputの場合の計算グラフしか得られず、control-flow (e.g. if-else) を含み計算グラフが動的に変化するようなモデルでは正しく動作しません。

例えば、以下のようなトイモデルを変換してみます。

class DecisionGate(torch.nn.Module):
    def forward(self, x):
        if x.sum() > 0:
            return x
        else:
            return -x

pytorch_model = DecisionGate()
traced_model = torch.jit.trace(pytorch_model, torch.ones(2))

traceの実行時にx = torch.Tensor([1.0, 1.0])を入力に使ったため、x.sum() > 0となり、そのままreturn xが実行されます。

tracingで変換されたモデルは、その時の計算グラフに固定されてしまいます。実際にtracingで変換されたモデルのIRをPythonシンタックスに直したコード (.codeプロパティ) を見てみると、条件分岐が実行されていない事が分かります。

print(traced_model.code)

Out:

def forward(self,
    x: Tensor) -> Tensor:
  return x

当然、入力の合計が負の場合はPytorchモデルとTracingで変換したモデルの出力は異なってしまいます。

input = -torch.ones(2)
print(pytorch_model(input))  # tensor([1., 1.])
print(traced_model(input))  # tensor([-1., -1.])

Scripting

計算グラフが動的に変わるようなcontrol-flowを含むモデルをTorchScript IRに変換する方法がscriptingによる変換です。

Scriptingでは計算グラフは介さず、Python(及びPyTorch)コードをパースして直接TorchScript IRに変換します。全てのPythonの型やシンタックスがサポートされている訳ではなく、Python一部の型・シンタックスで記述する静的型付け言語となっており、これがTorchScript Languageと呼ばれます。(これが最も多くTorchScriptと呼ばれています。)

いくつかの (単純な) control-flowもサポートされており、TorchScript Languageを使えば、control-flowもTorchScript IRに組み込む事が出来ます。

上述のDecisionGateを再び例に挙げて、scriptingで変換してみましょう。

If statementはサポートされているので、DecisionGateforwardメソッドはパースしてTorchScriptに変換する事が出来ます。

script_model = torch.jit.script(pytorch_model)
print(isinstance(script_model, torch.jit.ScriptModule))  # True

script_modelの方の.codeを確認してみましょう。

def forward(self,
    x: Tensor) -> Tensor:
  if bool(torch.gt(torch.sum(x), 0)):
    _0 = x
  else:
    _0 = torch.neg(x)
  return _0

if-elseも反映されている事が分かります。script_modelの方は、先ほどの入力(-torch.ones(2))を入れた場合にも結果はPytorchモデルと一致します。

input = -torch.ones(2)
print(pytorch_model(input))  # tensor([1., 1.])
print(script_model(input))  # tensor([1., 1.])

TorchScript module

Tracingもしくはscriptingで得られたtorch.jit.ScriptModuleC++torch::jit::Moduleのラッパーになっており、.save()メソッドでTorchScript IRの情報をシリアライズして保存しておけばC++からも同様に呼び出す事が出来ます。

torch::jit::script::Module module = torch::jit::load("<path>")

JITコンパイル

TorchScript IRは静的型付けなので、ある程度のコンパイラ最適化(デッドコード除去, 定数伝搬, ループ展開などの一般的なコンパイラ最適化のほか、element-wiseな演算のfusion, 行列積のバッチ化などの数値計算に特化した最適化)はできますが、ニューラルネットワークの計算においてはまだ不十分な情報があります。

例えば、

x + y + z

という単純なelement-wiseな計算でも、x, y, zの形状が全て(1000,)の場合はfuseした方が計算量、メモリアクセスともに少なくなりますが、x, yが(3,)で、zが(3, 1000)の場合にはfuseしない方が計算量は小さくなります(メモリアクセスは同じ)。

このようにテンソルの形状、auto-gradが有効なのか等に依存して最適化が異なりますが、型付けの情報からでは分かりません。

ラッキーな事に、通常のニューラルネットワークのモデルではそれほど動的な処理は行なわれない事が多いので、JITコンパイル時にテンソルの形状やauto-gradの有無などの情報 (profile) を記録し、そのprofileを元に最適化 (profile guided optimization) を行います。profile guided optimizationの詳細は文献に譲りますが、最初の数サンプルでprofileの取得とJITコンパイルが行われ、その後はprofile通りであれば最適化されたコードが、そうでなければ最適化されていない元のコードが実行されます。

では、どの程度処理速度、メモリ消費が改善されるのかをhuggingfaceのBERTを使って実験してみましょう。

BERTで実験

huggingfaceのドキュメントにはTorchScriptへの変換に関して、以下のように記載されています。

Exporting a model requires two things:

  • a forward pass with dummy inputs.
  • model instantiation with the torchscript flag.

1点目は、tracingの際に必要になる入力だとすぐに分かります。

2点目に関しては、Encoder-Decoderモデルでは、input tokenのembeddingレイヤーとoutput tokenのembeddingレイヤーのweightが共有されていますが、TorchScriptではweight sharingができないないそうです。そのため、このフラグがある場合にはモデルをインスタンス化する際に呼ばれるメソッドでoutputのembeddingレイヤーにはinputのembeddingレイヤの重みをcloneしています。

ドキュメントにも記載がある通り、decoderが無いモデルでは当然torchscriptフラグは必要がなく、BERTはEncoderモデルなので、今回は1のみの適当な入力でtracingを実行するだけで良さそうです。

ちなみに、scriptingを使ったBERTの変換はgenerator式(self.modules()がgeneratorになっている)のところでUnsupportedNodeErrorとなり、変換ができませんでした。

pytorch_bert = BertModel.from_pretrained("bert-base-uncased")
torch.jit.script(pytorch_bert)

Out:

UnsupportedNodeError: GeneratorExp aren't supported:
  File "/usr/local/lib/python3.7/dist-packages/transformers/modeling_utils.py", line 1538
        activations".
        """
        return any(hasattr(m, "gradient_checkpointing") and m.gradient_checkpointing for m in self.modules())
                   ~ <--- HERE

transformersのモデルがscriptingで変換できないという幾つかのissue(e.g. #5067)が上がっていますが、ライブラリの大幅な書き直しが必要なため、今のところ対応の見込みはないようです。

なので、今回はサポートされているtracingを使ってTorchScript moduleに変換しました。

Pytorch Profilerを使って実行速度、メモリ消費量を測定

colab環境で測定を行いました。GPUは不使用、測定に使う入力の長さは128、batch sizeは1としています。

token_length = 128
text = ' '.join(["test",] * (token_length-2))  # subtract [BOS] and [EOS]
inputs = tokenizer(text, return_tensors="pt")

Pytorch profilerのschedule設定は以下としました。

  • wait=5 : 最初の5ステップはprofilerをinactiveな状態でidlingする。
  • warmup=5 : profilerの起動直後はオーバーヘッドがあるので、その後5ステップは結果から除外する。
  • active=20 : その後の20ステップを実際の計測に使う。

※最初の数ステップはJITコンパイルが行われるため、オーバーヘッドが大きいのでwait/warmupで結果に含めないようにします。

with torch.profiler.profile(
    activities=[
        torch.profiler.ProfilerActivity.CPU,
    ],
    schedule=torch.profiler.schedule(
        wait=5,
        warmup=5,
        active=20,
    ),
    profile_memory=True,
) as prof:
    for _ in range(30):
        with torch.no_grad():
            pytorch_bert(**inputs)
        prof.step()

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))

PyTorchモデル、TorchScriptモデルでの実行結果はそれぞれ以下のようになりました。CPU toalとSelf CPUの違いは、Self CPUが該当演算のみの実行時間なのに対し、CPU totalは該当演算が呼んでいる演算を全て足した実行時間になっています。

PyTorch Model:

---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
                       Name    Self CPU %      Self CPU   CPU total %     CPU total  CPU time avg       CPU Mem  Self CPU Mem    # of Calls  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
              ProfilerStep*         4.09%     477.156ms       100.00%       11.653s     582.656ms         -80 b      -2.41 Gb            20  
               aten::linear         0.42%      48.971ms        86.53%       10.084s       6.907ms     810.06 Mb           0 b          1460  
                aten::addmm        83.95%        9.783s        85.69%        9.985s       6.839ms     810.06 Mb     810.06 Mb          1460  
               aten::matmul         0.10%      11.675ms         2.84%     331.225ms     690.052us     270.00 Mb           0 b           480  
                  aten::bmm         2.61%     304.199ms         2.61%     304.653ms     634.694us     270.00 Mb     270.00 Mb           480  
                 aten::gelu         1.98%     230.406ms         1.98%     230.406ms     960.025us     360.00 Mb     360.00 Mb           240  
                aten::copy_         1.93%     225.005ms         1.93%     225.005ms     112.502us           4 b           4 b          2000  
           aten::layer_norm         0.03%       4.021ms         1.43%     166.564ms     333.128us     187.50 Mb    -493.50 Kb           500  
    aten::native_layer_norm         1.33%     155.240ms         1.39%     162.543ms     325.086us     187.98 Mb      -6.50 Kb           500  
              aten::softmax         0.02%       1.764ms         1.05%     122.851ms     511.879us     180.00 Mb           0 b           240  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
Self CPU time total: 11.654s

TorchScript Model:

---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
                       Name    Self CPU %      Self CPU   CPU total %     CPU total  CPU time avg       CPU Mem  Self CPU Mem    # of Calls  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
              ProfilerStep*         0.05%       5.227ms       100.00%       10.656s     532.801ms         -80 b      -7.56 Mb            20  
                    forward         0.50%      53.352ms        99.95%       10.651s     532.539ms       7.56 Mb    -180.02 Mb            20  
               aten::linear         0.95%     100.736ms        89.45%        9.533s       6.529ms     360.06 Mb    -450.00 Mb          1460  
                aten::addmm        86.34%        9.201s        88.12%        9.390s       6.432ms     810.06 Mb     810.06 Mb          1460  
               aten::matmul         0.26%      28.168ms         3.12%     332.192ms     692.067us    -180.00 Mb    -450.00 Mb           480  
                  aten::bmm         2.72%     290.308ms         2.73%     290.623ms     605.465us     270.00 Mb     270.00 Mb           480  
                 aten::gelu         2.14%     227.959ms         2.14%     227.959ms     949.829us           0 b           0 b           240  
                aten::copy_         1.97%     209.491ms         1.97%     209.491ms     120.397us           0 b           0 b          1740  
              aten::softmax         0.45%      48.158ms         1.45%     155.002ms     645.842us           0 b    -180.00 Mb           240  
           aten::layer_norm         0.10%      10.234ms         1.45%     154.717ms     309.434us           0 b    -187.91 Mb           500  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
Self CPU time total: 10.657s

PyTorchモデルでは1ステップ平均583msでしたが、TorchScriptモデルでは1ステップ平均533msになっており、8.58%の速度改善になっていました。

また、メモリ消費量に関しても、linear(1ステップの合計)で810 Mb -> 360 Mbに削減されている、matmulでメモリ解放が優位になっている、GELU, Softmax, LayerNormではメモリ消費が0 bになっているなど、こちらも最適化されている事が分かります。(メモリ消費量が負の値になっているものはメモリ解放を意味しているようです。)

まとめ

今回はTorchScriptの調査を行ない、変換の原理や変換時に行われている処理などについて深掘りしてみました。また、huggingfaceのBERTをTorchScriptモデルに変換し、処理速度とメモリ消費量削減の測定を行いました。

BERTのケースでは8%以上推論速度が改善されましたが、本番環境において推論速度はコストに直結することもあるので、8%以上のコスト削減をPytorchのモデルを1度関数にかけるだけで実現できるのは非常に良い体験だと思いました。

一方で、transformersのような大きな外部ライブラリで、動的な計算グラフになるモデルを利用する場合、自分でTorchScript Languageで書き直すのは大変そうなので、そのような場合は前もってscriptingで変換できるのか確認しておくと良いでしょう。

長くなりましたが、最後までお読みいただきありがとうございます。

HTML でルビを描画する

はじめに

こんにちは、DevOps チームの中島です。
突然ですが、みなさんは HTML でルビが描画できることをご存知ですか?

私はこれまで知らなかったのですが、アジア圏のみの要望なのに標準化されているのは大変恐れ多いことですね。 ちなみにルビを描画する HTML は以下のようになります。エディタで普通に書こうとすると結構大変です。

<ruby>
  <rb>
    ルビを振る漢字
  </rb>
  <rp>
    ルビ非対応ブラウザのルビ開始記号左
  </rp>
  <rt>
    ここにルビ
  </rt>
  <rp>
    ルビ非対応ブラウザのルビ開始記号右
  </rp>
</ruby>

さて弊社では先日、日本語学習者のための日本語スピーキング力アセスメント (測定) サービス PROGOS Japanese をリリースしました。

アセスメントはブラウザ上に問題文が表示され、それに対する回答をマイクで録音する形式となっています。 日本語学習者の中には漢字を読むことが難しい方もいるため、問題文にルビが振ってあるのですが、 ルビ付きの HTML を作成するのが苦痛なのでツールを作成しました。今回はそのツールの仕組みを説明していきます。

技術要素の選定

以下のような考慮事項をもとに技術選定をしました。

  • A1. あまり手間をかけずに作れる、保守が大変にならない
    ツールを作る上でコスパを考慮するのは当然ですね。

  • A2. 外部のサーバにリクエストしてはいけない
    入力が試験問題であるという性質上、閉じた環境で動作する必要があります。

  • A3. 出力されたものが完全でなくても良い
    漢字の読み方は一意に特定することはできないので (ラノベのタイトルなどを想像してください) 、ある程度正しく推測されかつ自分で読み方を指定できれば十分です。

以上より、既に保守対象である GitLab サーバの Pages に HTML + JavaScript でホストし (A1) 、 ルビを振る外部 API を呼べないため (A2) 日本語辞書付きの形態素解析*1ライブラリ kuromoji.js を利用する (A1, A3) ことにしました。

ロジックの説明

ツールの動作としては、以下の通りとなります。

  • B1. 漢字かな交じりの文章を入力としてルビをカッコ付きで付与した形式に変換する
    日本語で話してください→日本語(にっぽんご)で話(はな)してください
  • B2. 次に変換された内容に対して必要であればユーザがルビの細かい修正を行う
    →日本語(にほんご)で話(はな)してください
  • B3. 最後にそれを HTML に変換する
    <ruby>...(省略)...</ruby>

ここではメインのロジックとなる B1 の部分について説明します。
B1 の部分で行いたい処理は、漢字かな交じり文に出現する各漢字に対して、(概ね) 適切な読み方を与える処理です。

辞書から読み方を取得する

まずは日本語辞書に登録された読み方を各漢字に割り当てることを試みます。
以下のように kuromoji.js を利用すると、形態素解析が行われた上で辞書に登録されている読み方を取得できます。

var kuromoji = require("kuromoji");
kuromoji.builder({ dicPath: "./node_modules/kuromoji/dict" }).build(function (err, tokenizer) {
    var path = tokenizer.tokenize("美味しそうな焼肉を指し示した");
    path.map((token) => {
        console.log(token.surface_form + ": " + kana2hira(token.reading))
    });
});

※kana2hira (カタカナをひらがなに変換する関数) の実装は省略

出力

$ node index.js
美味し: おいし
そう: そう
な: な
焼肉: やきにく
を: を
指し示し: さししめし
た: た

なるほど、形態素は以下のパターンに分けられることが分かります。

  • C1. 全て漢字
    焼肉
  • C2. 全てひらがな
    そう
  • C3. 漢字が複数続いたあとひらがなで終わる
    美味し
  • C4. 3.が複数繰り返される
    指し示し

これらのパターンに対して、漢字に対する読み方の割り当てを考えていきます。

C1 は熟語と考え、構成する全ての漢字に対して辞書が振った読み方を割り当てれば問題なさそうです。
焼肉 / やきにく
→焼肉(やきにく)

C2 は漢字がないので割り当てる必要ありません。
そう
→そう

C3 は後方からひらがなをマッチングしていき、残った部分が漢字の読み方になります。
美味|し /
おい|し
→美味(おい)し

C4 は特に今回の例の場合は、機械的に決められないためひと工夫必要そうです。
指し示し / さししめし
→指(さ)し示(しめ)し ?
→指(さし)し示(め)し ?

常用漢字一覧から読み方を取得する

C4. を一意にするために必要な情報は、各漢字の単体での読み方です。
Wikipedia常用漢字一覧 から、漢字の読み方リストを作成します。

読み方の推測

あとは C3 と同様に元の文章と kuromoji の読みを後方からマッチングしていき、漢字が出てきたら (図の「示」の部分) その漢字の読み方リストから同じ読み方があるかを探します。 同じ読み方があれば (「しめ」 が該当) その文字数分消費して前方に向かって同様の処理を繰り返します。ない場合その形態素の読み方を推測するのは諦めます。

以上が読み方を推測する処理になります。

終わりに

手前味噌ですが、今回は自分の持っている手札をうまく組み合わせて、最低限の努力で最大の効果を出せた気がしています。 これからもどんどん出来ることを増やして、価値を生み出していきたいですね。
最後までご覧頂きありがとうございました。

We're hiring!

rarejob-tech.co.jp

*1:形態素解析についての説明は他所に譲ります。

Auth0を利用して、社内業務で利用するシステムに、ロールベースアクセス制御を導入してみた

こんにちは。レアジョブ英会話開発グループの越です。

私がよく行くサウナ室にもテレビがありますが、サウナ室のテレビはチャンネルを変更することができないですね。

つまりサウナーには番組を選ぶ権限が無いが、サウナ室の管理者にはチャンネルを変更する権限があるということ。

この「ユーザーができることを制御する」ということを、ITシステム内ではアクセス制御ともいいますね。直近で社内の業務で利用するシステムにロールベースアクセス制御を導入する業務に携わったので、そのときにAuth0を利用したことについての記事を書きたいと思います。

RBAC(ロールベースアクセス制御)とは

Role Based Access Control の略

アクセス制御方式、あるいはアクセス制御モデルの1つ。

RBACの他にはDACや、MACがある。

RBACはシステムを利用する主体(ユーザーやサービス)に直接パーミッションを設定するのではなく、ロールにパーミッションを設定して、アクセスするシステムを利用する主体にはロールを設定する権限管理方法のことです。

ロールベースアクセス制御とは、情報システムのアクセス制御の方式の一つで、個々の利用者ごとではなく、利用者に割り当てられたロール(role:役割)ごとに権限を付与する方式。

ロールベースアクセス制御(RBAC / 役割ベースのアクセス制御)とは - 意味をわかりやすく - IT用語辞典 e-Words

DAC(任意アクセス制御)

Discretionary Access Control の略

任意アクセス制御は、リソースの所有者にアクセス制御を任せる方式です。

任意アクセス制御の代表的な例としては、Unix 系 OS などで実装されているファイルパーミッション

MAC(強制アクセス制御)

Mandatory Access Control の略

管理者一人がアクセス制限を行う方法です。

強制アクセス制御は、リソース所有者の意図に関らず、システムの管理者により一定のアクセス制御を強制する方式です。

強制アクセス制御の例としては SELinux

ロールベースアクセス制御を行うメリット

ユーザーに対して直接パーミッションが与えられるわけではなく、ロールを通して与えられるため、各人のアクセス権の管理はユーザーへのロールの適切な割り当てに単純化され、ユーザーアカウントの追加やユーザーの部門間の異動などにも容易に対応できる。

ロールベースアクセス制御 - Wikipedia

社内の業務システムには、機密の情報や、利用者を限定したい機能が存在します。それらを保護しようとする際、保護が厳しすぎると業務が滞ります。逆に保護が緩すぎると、致命的なセキュリティの問題が発生する可能性があります。

ロールベースアクセス制御を行うことで、システムを利用するユーザーにパーミッションを割り当てる際のエラーの可能性を減らすことができると、Auth0のドキュメントにも記載があります。

auth0.com

Auth0でロールベースアクセス制御を行ってよかったところ

Auth0という枠組みの中でRBACの導入を進めたので、RBACを導入するにあたって、設計や開発を行う過程で致命的な技術的負債を埋め込まない安心感がありました。

認証には会社のGoolgleアカウントでのログインを行う必要があり、SaaSのサービスを利用した認証機能を検討していました。

Auth0を利用することで、認証と認可について開発や保守の観点でのコストが不要になり、ロールやパーミッションの設計に注力ができたのがよかったです。

加えてライブラリ、ドキュメント、APIが充実しているため、他のメンバーへの情報共有に関してもコミュニケーションコストは少なかった点について、導入して良かったと感じました。

ロールとパーミッションの設計

やることとしては、APIの登録、APIパーミッションを定義、ロールの作成、ロールをユーザーにアサインの4つを順番に行います。

具体的な手順はドキュメントに記載されているので省略します。

ドキュメントのとおりに進めるだけなので、やることはほとんど無かったのですが、ロールとパーミッション命名規則だけ、設計してから進めました。

  1. Register API with Auth0
  2. Define permissions for API
  3. Create roles
  4. Assign roles to users

auth0.com

パーミッション命名規則

パーミッション名は参照、作成・更新、削除の3つの操作で分けて、機能やリソースに対する操作ごとに権限を分けられるようにしています。

read|write|delete:機能名|リソース名
パーミッション名(以下は例です) 説明
read:payments paymentsリソースの参照権限
write:payments paymentsリソースの作成・更新権限
delete:payments paymentsリソースの削除権限

ロールの命名規則

ロールの名前で、ある程度どのような人が使用しているか判別できるように、ロールの命名規則は、所属組織・プロダクト名・ロール名の3つをアンダーバーで区切る形で作成しました。

レアジョブ英会話では一部の社内業務を外部に委託しており、業務内容は同じでも所属する組織が異なるというケースがあるため、所属組織ごとにロールを管理できるように設計しました。

所属組織_プロダクト名_ロール名
ロール名(以下は例です) パーミッション 説明
rarejob_rarejob-eikaiwa_payment-supervisor read:payments
write:payments
delete:payments
レアジョブ英会話の支払い業務に携わる管理者のロール
rarejob_rarejob-eikaiwa_payment-staff read:payments
write:payments
レアジョブ英会話の支払い業務に携わるスタッフのロール
rarejob-tech_rarejob-eikaiwa_developer read:payments レアジョブ英会話の開発に携わる開発者のロール

実装の観点で意識したこと

ログイン時の権限取得

AccessToken(jwt)を要求したときに、そのclaimにpermissionsという属性が追加されています。

{
  ...
  "permissions": [
    "read:payments",
    "write:payments"
  ]
}

ログイン時に取得したアクセストークンをセッションストレージでキャッシュして、アプリケーション側で機能の出し分けにパーミッションを利用していきます。

auth0.com

Laravelフレームワークとの親和性

LaravelのサンプルアプリケーションやチュートリアルがAuth0側で用意されており、それを参考にすることで簡単に導入できたのも良かったです。

各機能のアクセス許可

パーミッションの使い方は各機能のアクセス許可に使用します。セッションストレージでキャッシュしたアクセストークンを参照して、必要なパーミッションを持っているかどうか、Laravelのmiddlewareでチェックします。

メニューやボタンの出し分け

パーミッションの使い方は機能のアクセス許可だけではなく、メニューやボタンの出し分けにも使います。これもセッションストレージでキャッシュしたアクセストークンから必要なパーミッションを持っているかチェックして、ビュー側で出し分けすることができます。

まとめ

個人的に業務で初めてアクセス制御を扱ったので、手探りな中進めていくことに最初は不安がありましたが、Auth0の仕組みに乗せることで、技術的な不安要素はほぼ無くせたのが良かったと感じました。業務を通してアクセス制御に関する理解が深まったのもよかったです。

GCPリソースを CDK for Terraform で作成する

はじめまして、DMP (データマネジメントプラットフォーム) グループの すぎみつ です。

最近はスキルアップ手当*1で昇降デスクの購入を検討しています。電動式昇降デスクのすゝめによると自分に最適なデスクの高さは74cmでした。

さて、DMP グループでは現在 GCP のリソース管理に CDK for Terraform を利用していまして、CDK for Terraform の利用例についてご紹介します。

はじめに

DMP グループはレアジョブグループのデータ基盤の整備をメイン業務の一つとしています。 現在は既にあるデータ基盤を新たに BigQuery を中心とした Google Cloud Platform に移行しています。
レアジョブ英会話を始めとした各サービスは AWS で構成されており、収集対象となるデータは AWS から GCP に転送する流れとなります。
今回詳細には触れませんが、データ転送を行うパイプラインは AWS 側にあり、リソースの管理は AWS CDK で行っています。弊社の AWS リソースの管理は殆ど AWS CDK で統一されておりそれに倣っています。
AWSGCP を扱う環境なので、IaC は Terraform や Pulumi といったマルチクラウド対応のサービスを使いたいところですが、同じように書ける CDK for Terraform を利用することにしました。

CDK for Terraformとは

CDK for Terraform は簡単に言うと HCL を書かなくても、AWS CDK で書ける Terraformです。マルチクラウド対応する CDK になります。
www.terraform.io

AWS CDK とコンポーネントを共有していて、AWS CDK のようにコードで書いた定義を Terraform の HCL プロジェクトで利用する インフラ構成ファイル( Json ファイル)に変換し、Terraform でリソースの作成を行えます。

CDK for Terraform でのプロジェクト作成からデプロイまでの一連の操作は CDKTF CLI で行います。 https://mktg-content-api-hashicorp.vercel.app/api/assets?product=terraform-cdk&version=v0.11.2&asset=website%2Fdocs%2Fcdktf%2Fconcepts%2Fimages%2Fcdktf-terraform-workflow.png&width=4096&height=3070 https://www.terraform.io/cdktf/concepts/cdktf-architecture

サンプルの紹介

簡単にサンプルを紹介します。

サービスアカウントと GCS バケットの作成し、作成した GCS バケットに対してサービスアカウントからのアクセス権を許可する例になります。

CDK For Terraform のインストールとそれに必要な準備は公式の以下リンクを参照してください。
Install CDK for Terraform and Run a Quick Start Demo | Terraform - HashiCorp Learn

プロジェクトの作成

CDKTF CLI を実行していきます。 今回のプロジェクトは TypeScript で作成します。

$ cdktf init --template=typescript

プロジェクト初期化後のディレクトリ構成は以下のようになります。
cdk_for_tf というディレクトリ名以下にプロジェクトを作成しました。

.cdk_for_tf
├── __tests__
├── cdktf.json
├── help
├── jest.config.js
├── main.ts
├── node_modules
├── package-lock.json
├── package.json
├── setup.js
└── tsconfig.json

実装

修正後の main.ts は以下になります。

import { Construct } from "constructs";
import { App, TerraformStack, GcsBackend } from "cdktf";
import { 
  GoogleProvider, 
  ServiceAccount, 
  StorageBucket, 
  StorageBucketIamBinding 
} from "./.gen/providers/google";


class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    // リモートバックエンドの指定
    new GcsBackend(this, {
      bucket: 'cdktf-example-remote-backend',
      prefix: 'state',
    })

   const provider = new GoogleProvider(this, 'Set GCP provider',{
     project: `${project_id}`
   })

   // サービスアカウントの作成
   const serviceAccount = new ServiceAccount(this, 'Create Service Account',{
      provider: provider,
      accountId: 'cdktf-example-service-account',
      displayName: 'cdktf-example-service-account',
   })

    // GCSバケット作成
    const bucket = new StorageBucket(this, 'Create GCS Bucket', {
      provider: provider,
      location: 'ASIA-NORTHEAST1',
      name: 'cdktf-example-gcs-bucket',
      uniformBucketLevelAccess: true,
      dependsOn: [serviceAccount] 
    })

    // GCSバケットへの権限追加
    new StorageBucketIamBinding(this, 'Add role roles/storage.legacyBucketReader',
    {
      provider: provider,
      bucket: bucket.name,
      role: 'roles/storage.legacyBucketReader',
      members: [`serviceAccount:${serviceAccount.email}`],
      dependsOn: [serviceAccount, bucket]
    })

    // オブジェクトへの権限追加
    new StorageBucketIamBinding(this, 'Add role roles/storage.objectViewer',
    {
      provider: provider,
      bucket: bucket.name,
      role: 'roles/storage.objectViewer',
      members: [`serviceAccount:${serviceAccount.email}`],
      dependsOn: [serviceAccount, bucket]
    })
  } 
}

const app = new App();
new MyStack(app, "cdk_for_tf");
app.synth();

デプロイ

deploy コマンドで構成ファイルの生成及びリソースの作成まで纏めてできるためcdktf deployを実行します。尚、GCP のクレデンシャルは gcloud の認証で済ませているものとします。

$ cdktf deploy
cdk_for_tf  Initializing the backend...
cdk_for_tf  
            Successfully configured the backend "gcs"! Terraform will automatically
            use this backend unless the backend configuration changes.
cdk_for_tf  Initializing provider plugins...
            - Finding hashicorp/google versions matching "4.25.0"...
cdk_for_tf  - Using hashicorp/google v4.25.0 from the shared cache directory
cdk_for_tf  Terraform has created a lock file .terraform.lock.hcl to record the provider
            selections it made above. Include this file in your version control repository
            so that Terraform can guarantee to make the same selections by default when
            you run "terraform init" in the future.
cdk_for_tf
cdk_for_tf  Terraform has been successfully initialized!
cdk_for_tf  Terraform used the selected providers to generate the following execution
            plan. Resource actions are indicated with the following symbols:
            + create
            Terraform will perform the following actions:
cdk_for_tf    # google_service_account.CreateServiceAccount (Create Service Account) will be created
              # google_storage_bucket.CreateGCSBucket (Create GCS Bucket) will be created
              # google_storage_bucket_iam_binding.Addroleroles--storagelegacyBucketReader (Add role roles--storage.legacyBucketReader) will be created
              # google_storage_bucket_iam_binding.Addroleroles--storageobjectViewer (Add role roles--storage.objectViewer) will be created

            Plan: 4 to add, 0 to change, 0 to destroy.
          ───────────────────────────────────────────────────────────────────
            Saved the plan to: plan

            To perform exactly these actions, run the following command to apply:
            terraform apply "plan"
cdk_for_tf  google_service_account.CreateServiceAccount (Create Service Account): Creating...
cdk_for_tf  google_service_account.CreateServiceAccount (Create Service Account): Creation complete after 2s 
cdk_for_tf  google_storage_bucket.CreateGCSBucket (Create GCS Bucket): Creating...
cdk_for_tf  google_storage_bucket.CreateGCSBucket (Create GCS Bucket): Creation complete after 2s 
cdk_for_tf  google_storage_bucket_iam_binding.Addroleroles--storagelegacyBucketReader (Add role roles--storage.legacyBucketReader): Creating...
            google_storage_bucket_iam_binding.Addroleroles--storageobjectViewer (Add role roles--storage.objectViewer): Creating...
cdk_for_tf  google_storage_bucket_iam_binding.Addroleroles--storageobjectViewer (Add role roles--storage.objectViewer): Creation complete after 9s 
cdk_for_tf  google_storage_bucket_iam_binding.Addroleroles--storagelegacyBucketReader (Add role roles--storage.legacyBucketReader): Creation complete after 9s 
cdk_for_tf  
            Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

deploy が成功しました。
出力を見ると内部では構成ファイルを作成してから、terraform planの実行後、terraform applyが実行されているのが確認できますね。

結果確認

デプロイされた結果を確認します。

# バケット一覧確認
$ gsutil ls
gs://cdktf-example-gcs-bucket/
gs://cdktf-example-remote-backend/  # リモートバックエンド用に予め作成したバケット

# バケットのメタ情報確認
$ gsutil ls -L -b gs://cdktf-example-remote-backend/
gs://cdktf-example-remote-backend/ :
        Storage class:                  STANDARD
        Location type:                  region
        Location constraint:            ASIA-NORTHEAST1
        ~省略~
        Public access prevention:       enforced

バケットに対する権限を確認します。(コマンド結果は一部省略しています)

$ gsutil iam get gs://cdktf-example-gcs-bucket/
{
  "bindings": [
    {
      "members": [
        "serviceAccount:cdktf-example-service-account@${project_id}.iam.gserviceaccount.com"
      ],
      "role": "roles/storage.legacyBucketReader"
    },
    {
      "members": [
        "serviceAccount:cdktf-example-service-account@${project_id}.iam.gserviceaccount.com"
      ],
      "role": "roles/storage.objectViewer"
    }
}

想定通りにリソースが作成できたことを確認できました。

懸念するところ

  • 商用サポートはない
  • 利用者が少ない気がする
    • Web 上の記事が少ないです。この記事もそうですがあってもエントリ的なものになっています。

今利用している GCP サービスの種類も構成も大きく無く、致命的な問題に遭遇していないですが、今後利用を拡大していく上で障壁になる可能性はあります。バグ判断や代替策案を講じるために費やす時間の大きさを懸念するところではあります。
お前の技術力次第だろと言われればおしまいですが。。。

おわりに

今回は CDK for Terraform の簡単な例を紹介をしました。
今後は GCP の基盤も拡張されていくので、運用を踏まえた具体的な良し悪しを紹介できたらと思います。

*1:弊社の福利厚生の1つでスキルアップ用途だけなく、在宅ワーク環境の整備のためにも利用できます。

Streamlitで時系列予測アプリを作る

こんにちは、EdTech Labの齋藤です。

レアジョブテクノロジーズ社となって四半期が経過しそうという状況です。時が経つのは早いですね。体感速度マッハ10.2です。

というわけで、今回はトップガン マーヴェリックがいかに最高の映画だったかということをお伝えしたいのですが、我慢してStreamlitを利用したWebアプリケーションを作成していきます。 ここ数ヶ月で社内の業務効率化を行うためのアプリを作成していたのですが、その際利用したものがStreamlitでとても使い勝手が良かったのでこの場を借りて紹介していこうと思います。

実際に作成した業務効率化アプリを載せるわけにはいかないので、代わりにDartsというライブラリを利用して、時系列予測ができるアプリを作っていきます。

Streamlitとは


Pythonで利用可能なフレームワークで、フロントエンドの知識の少ない人でもさくっとWebアプリを作れるサービスです。 公式サイトのWeave in interactionにもあるように、ウィジェットを追加するのもPythonのみで完結するお手軽さです。 また、公式のチートシートも充実しており、とっつきやすくなっています。

streamlit.io

Dartsとは


Pythonで利用可能なライブラリで、複数種類の時系列モデルを利用することが可能なライブラリです。 利用方法もシンプルで、ちょっと時系列モデルを試してみたいというときに非常に便利です。

unit8co.github.io

こいつらを組み合わせて、お手軽時系列予測アプリを作っていきましょう。

実装


DartsのチュートリアルをStreamlitに落とし込んでいきたいと思いますが、それだけでは寂しいので複数の時系列モデルをラジオボタンで切り替えられるものを作ろうかなと思います。 時間の都合上、利用するモデルの説明及びハイパーパラメータ の探索は省きます。

import streamlit as st

from darts import TimeSeries
from darts.models import ExponentialSmoothing, ARIMA, AutoARIMA, BATS, Theta

from sklearn.model_selection import train_test_split
import pandas as pd
import matplotlib.pyplot as plt

st.title('Dartsで時系列予測')

# Dartsの処理
# モデルの選択
model_select = st.radio(
    "使用するモデルを選んでください",
    ("ExponentialSmoothing", "ARIMA", "AutoARIMA", "BATS", "Theta"))
if model_select == "ExponentialSmoothing":
    model = ExponentialSmoothing()
    st.write("確率論的モデルです。サンプリング回数はどうしますか?")
    num_samples = st.number_input("num_samples", value=1)

elif model_select == "ARIMA":
    model = ARIMA()
    st.write("確率論的モデルです。サンプリング回数はどうしますか?")
    num_samples = st.number_input("num_samples", value=1)

elif model_select == "BATS":
    model = BATS()
    st.write("確率論的モデルです。サンプリング回数はどうしますか?")
    num_samples = st.number_input("num_samples", value=1)
# num_samplesを持たないモデル
elif model_select == "AutoARIMA":
    model = AutoARIMA()
    num_samples = 1
elif model_select == "Theta":
    model = Theta()
    num_samples = 1


# csvをアップロード
csv = st.file_uploader('Upload a CSV')

if csv is None:
    st.write("ファイルをアップロードしてください")
else:
    df = pd.read_csv(csv, delimiter=",")
    train, val = train_test_split(df, shuffle=False)
    train, val = TimeSeries.from_dataframe(train, 'Month', '#Passengers'), TimeSeries.from_dataframe(val, 'Month', '#Passengers')

    model.fit(train)
    prediction = model.predict(len(val), num_samples=num_samples)

    low_quantile = st.number_input("low_quantile", value=0.05, format="%f")
    high_quantile = st.number_input("high_quantile", value=0.95, format="%f")

    # 図の描画
    fig, axes = plt.subplots()
    series = TimeSeries.from_dataframe(df, 'Month', '#Passengers')
    series.plot()
    prediction.plot(label='forecast', low_quantile=low_quantile, high_quantile=high_quantile)
    plt.legend()
    plt.tight_layout()
    st.pyplot(fig)

できました。コーディングはこれだけです。

st.radio(("A", "B", "C"))

のようにするだけでラジオボタンが生成でき、上記のように書くことで選択された処理を行なってくれます。

Dartsの方は注意点として、データをインプットする際はfrom darts import TimeSeriesを利用する必要がありそうでした。

こいつのファイル名をstreamlit.pyとしたとき、streamlit run streamlit.pyをコマンドで実行します。 すると、8501ポートでアプリケーションサーバが起動し、Webで利用可能になります。

動作確認


というわけで、実際どんな感じかみていきましょう。

いざstreamlit run streamlit.py

fig.1
良い感じですね。

fig.2
ラジオボタンでのモデル切り替えると選択したモデルに合わせて項目が増減するのが確認できます。

サイドが寂しいな〜と思ったらst.sidebar.write('サイドバーが寂しい')のようにsidebarという一言を加えるだけで

fig.3
とサイドバーが作れます。これで寂しくないですね。ここにモデル選択用のラジオボタンを置くとかしても良いかもしれません。

データのアップロードと結果


今回はチュートリアルに沿っているので、そのままAirPassengersのデータを利用します。お馴染みですね。データのフォーマットはこんな感じです。

fig.4

画像のBrowse filesをクリックすると手持ちのファイルが選択できるのでAirPassengers.csvをアップロードしてみましょう。

fig.5

うまく予測ができていそうです。low_quantile, high_quantileを変更することで信頼区間を変えられるようにもしておきました。

上記のような簡単な実装でも、そこそこリッチなウェブアプリケーションが出来上がりました。 今回はDartsのチュートリアルをベースにしているので、AirPassengersのデータ形式のみを受け付けるようにしていますが、もちろんデータの処理を変えれば汎用的な分析・予測アプリが作れます。

アプリのデプロイ for AWS Fargate


実際に業務で作成したアプリは、実用に足るアプリにするために以下のサイトを参考にしつつ、弊社DevOpsチームから多大なご助力を得ましてAWS Fargateでデプロイしました。 towardsdatascience.com

最後に


このStreamlitを利用すれば、今回のように実際のアプリとして動かすだけではなく、分析結果の共有やインタラクティブダッシュボードの作成、プロトタイピングというように様々な場面で活用が期待できるフレームワークだなと改めて感じました。地味KPI を追ったりするのにも使えそうですね。

というわけでここまでお付き合いいただきありがとうございました!

地味KPIを追う

ハロー、CTOです。今日は地味KPIの話をしようと思います。 我々のようなインターネット事業者は常にプロダクトの売上や利益といった数字に対して一喜一憂し、その改善に笑顔し、時には涙を流しながらプロダクトの運営をしています。その中で効果的に組織で事業を伸ばしていくためにKPIのようなものを設定することは一般的なことかと思います。 その中でも当たり前に計測しているもの(インフラパフォーマンスや事業数値)を除いて「良くて当たり前」「下がったらまずい」といったある種、計測していることすら時に忘れてしまうような地味〜〜な数値たちもあり、これを私の中ではこっそり「地味KPI」と捉え、まさに地味に優先度設定し可視化や改善をしています。今回はプロダクトの安定性に関する地味KPIをご紹介します。

隠れている大事な地味KPI

そもそもの地味KPIの目的はチームのデリバリを加速し、意思決定の質を上げプロダクトの安定性を良くするためです。 あえて「プロダクトの安定性」と書いたのは「システムの安定性」はすでにMTBFMTTRのような一般的な指標や、APIのレスポンスタイムやサーバのパフォーマンスなどすでにクラウドベンダが定義しているレベルで当たり前に計測されている一方で、プロダクト・アプリケーション固有な計測しておくべき安定数値はあまり言及されないためです。 プロダクトの質によって「ユーザーさんが正しく使えているか」といった問に対してE2EやUTなどでケアする以外にも地味KPIをしっかり設定して、これをモニタリングし、デリバリの正しさを常に保証することで対内外にプロダクトの安定性を伝えることができます。

例えば

地味KPIはプロダクトや見るべき職責で様々な指標があります。 例えばECを例にしたときに、売上やDAUなどは直接事業に関わる事が多く、変動の検知はデイリーで見て気づきやすくもありますが、一方で例えば「お気に入り機能が利用されているかどうか」などは即日常に計測されているKPIに影響はないですが中長期的・間接的にKPIや問い合わせに影響を及ぼし、主KPIから見えにくいサービスからの離脱を生みます。 サブスクのサービスにおいても一部の機能が使えない等で、即時離脱や休会に利用はないがボディブローのようにじわじわとユーザーさんや運用に悪影響を及ぼす数値の低下は一定数存在します。

レアジョブにある地味KPI

1. レッスンルームの接続率

弊社のレアジョブ英会話のプロダクトで、レッスンをするためのレッスンルームという機能があります。 レッスンルームはSPAで動いており、クライアントアプリケーションのためその挙動が正しく動いていたかなどをシステム上把握しにくい事が多いです。 特に接続が実施できたかどうか、などは顧客環境によっては明確に失敗することもあり、100%稼働が期待値ではありません。一方でもし保証している環境内で不具合が起きていて、接続に失敗している場合などは適切に検知し対策を打つ必要があります。またこれはリリースをしなくてもWebRTCのようなブラウザ依存が強い技術を使っており、ブラウザ自体のアップデートやリリースにでも地味KPIが変動します。 弊社ではこの接続率をWebRTCの接続のプロセスで発生するイベントを記録し、レッスン時間帯毎・日時・月次・顧客ごとにredashで可視化、それをnodeで書いたbotで自動でchatworkに投稿し、大きく変動があればスレッドの参加者にメンションを飛ばす仕組みを入れています。どのセグメントで切ってもほぼほぼ99%を超えてくる数値ですが、時折異常値を出すこともあり、これにより異常を早期に検出し対応することができてきました。

この仕組があるおかげでリリース後の最低限の動作保証や、ブラウザなどの変更による外部影響も早期に検出できるので強気にリリースやトライができます。

2. 検索実施数

検索の利用は弊社でもかなり多く、毎月数十万レッスンを提供する弊社では同じように講師検索の機能利用数も非常に多いです。 一方でレッスンの予約数のようなクリティカルな機能は問題があればCSへの問い合わせですぐ気づくことはできますが、検索は導線も多く特定箇所や条件で使えなくてもすぐ検知ができません。 また講師の開講状況やトレンドにも依存はしますが、全体の傾向は常にある程度は一定なので、変化があれば不具合や不整合などが原因だとわかります。この悪化は問い合わせでなく離脱に直結することもあり、地味に見逃せません。

検索ページのリニューアルを実施した際も効果測定の観点だけではなくて安定性の観点でこのモニタリングを実施したりもしていました。

まとめ

これらの地味KPI管理は正直やらなくてもなんとかなります。プロダクトは複雑に絡み合っており、特定領域がダメでも他の手段ややり方である程度成立させることはできるからです。またケースによっては主KPIから逆算で問題に気づくこともできます。 一方で計測しにくい不具合を早期に気づいたり、リリースを保証しデリバリの速度や頻度を上げやすくなったり、短期ではわからないが中長期的に主KPIに影響のある事象に気づけるなどまさに地味だけどとても大事な成果を得ることができると思っています。 マネジメント観点でもチームの管理するプロダクトを定量的に改善を計測できるのでおすすめです。 なんとなく大丈夫だと思っていても、計測して傾向を時系列で見てみるとこれまで見えてこなかったユーザーさんやプロダクトのインサイトに気づくことが多いのでぜひ。

Let's 地味!