SageMaker用のコードをローカルで動かす – scikit-learnの決定木でアヤメの種類を分類
SageMakerはローカルで使うことができるので、それを試してみた。 この記事を書くにあたって以下の公式の記事を参考にしています。 オンプレミス環境から Amazon SageMaker を利用する 機械学習のHelloWorld アヤメデータをschikit-learnの決定木分類器で学習して種類を予測する. 結構いろいろなところで機械学習のHelloWorldとして使われている例題を題材にしていく. sagemaker-python-sdk/scikit_learn_iris 既に SageMaker用のサンプルコードがあるので、 これをローカルで学習・推論できるように修正していく。 構成は以下の通り. 公式のブログの通り、SageMaker Notebook用に書かれた.ipynb を Local用に微修正するだけで動く。 SageMaker Notebookで動かすための.ipynb .ipynbから呼ぶschikit-learnコード SageMakerのサンプルはSageMakerのJupyterNotebookで動くように書かれているがが、 ちょっと修正するだけでローカルで動くようになる様子。(1つしか試してないけど) 前準備 学習と推論をローカルで行うが、そのために裏でDockerのコンテナが走る。 ローカルコンピュータ用にDockerをインストールしておく必要がある。 CredentialsとIAM 以下が必要。 AmazonSageMakerFullAccess 権限をもった IAM ユーザの Credential AmazonSageMakerFullAccess の IAM ロール ローカルコードからAWSリソースにアクセスするために aws configure を使って設定する。 Credentialsが書かれたcsvをダウンロードし aws configure の応答に答えていく。 $ pip install awscli --upgrade --user $ aws configure AWS Access Key ID [None]: ****************** AWS Secret Access Key [None]: ************************************ Default region name [None]: ap-northeast-1 Default output format [None]: json SageMaker PythonSDKインストール SageMaker PythonSDKをインストールする。 実行するコードに応じてSDKのバージョンを指定することができる。 $ pip install -U sagemaker >=2.15 バージョンを指定しない場合は以下の通り。 $ pip install sagemaker ローカルのJupyter Notebookでファイルを修正 scikit_learn_estimator_example_with_batch_transform.ipynb を ローカルのJupyter Notebookで修正していく。 SageMaker ローカルSessionを開始 SageMakerを想定したコードは以下。 # S3 prefix prefix = \"Scikit-iris\" import sagemaker from sagemaker import get_execution_role sagemaker_session = sagemaker.Session() # Get a SageMaker-compatible role used by this Notebook Instance. role = get_execution_role() それをローカルで動かすために以下のように修正する localSession()というセッションが用意されているのでそれを使用する。 ローカルでは get_execution_role()では ロールを取得できないので直接ロールのARNを指定する。 # S3 prefix prefix = \"Scikit-iris\" import sagemaker from sagemaker import get_execution_role # LocalSession()を使用する sagemaker_session = sagemaker.local.LocalSession() # sagemaker.Session()から変更 # Get a SageMaker-compatible role used by this Notebook Instance. # ローカルでは get_execution_role()は使えない。直接ロールのARNを指定する。 # role = get_execution_role() role = \'arn:aws:iam::(12桁のAWSアカウントID):role/(ロール名)\' 学習用データの準備 (変更なし) 学習用データが巨大であればS3にデータを準備する(と書かれている). アヤメデータは軽量なので、ローカルファイルに保存する。 import numpy as np import os from sklearn import datasets # Load Iris dataset, then join labels and features iris = datasets.load_iris() joined_iris = np.insert(iris.data, 0, iris.target, axis=1) # Create directory and write csv os.makedirs(\"./data\", exist_ok=True) np.savetxt(\"./data/iris.csv\", joined_iris, delimiter=\",\", fmt=\"%1.1f, %1.3f, %1.3f, %1.3f, %1.3f\") その後、用意したローカルデータをSageMaker Python SDKに食わせる。 WORK_DIRECTORY = \"data\" train_input = sagemaker_session.upload_data( WORK_DIRECTORY, key_prefix=\"{}/{}\".format(prefix, WORK_DIRECTORY) ) Scikit learn Estimator scikit-learnの機械学習は以下の3段構成になっている。 Estimator: 与えられたデータから学習(fit)する Transformer: 与えられたデータを変換(transform)する Predictor: 与えられたデータから結果を予測(Predict)する SageMakerは機械学習プラットフォームであって、かなり多くのライブラリや手法がサポートされている。 その中で、scikit-learnもサポートされていて、SKLearn Estimatorとして使用できる。 要は、schikit-learn のI/Fに準じたコードを SageMaker に内包することができる。 SKLearn Estimatorに scikit-learn コードを食わせると SageMakerから SKLearn インスタンスとして操作できる. 例えば、.ipynbで以下のように書く. from sagemaker.sklearn.estimator import SKLearn FRAMEWORK_VERSION = \"0.23-1\" script_path = \"scikit_learn_iris.py\" sklearn = SKLearn( entry_point=script_path, framework_version=FRAMEWORK_VERSION, instance_type=\"local\", role=role, sagemaker_session=sagemaker_session, hyperparameters={\"max_leaf_nodes\": 30}, ) SKLearnにentry_pointとして渡しているのがscikit-learnのコード本体。 内容は以下。普通の決定木分類のコードにSageMakerとのIFに関わるコードが追加されている。 実行時引数として、SM_MODEL_DIR、SM_OUTPUT_DATA_DIR、SM_CHANNEL_TRAINが渡される。 fitで学習した結果(つまり係数)をシリアライズしSM_MODEL_DIRに保存する。 model_fnでは、SM_MODEL_DIRにシリアライズされた係数をデシリアライズし、 scikit-learnの決定木分類木オブジェクトを返す。 # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the \"License\"). # You may not use this file except in compliance with the License. # A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the \"license\" file accompanying this file. This file is distributed # on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either # express or implied. See the License for the specific language governing # permissions and limitations under the License. from __future__ import print_function import argparse import os import joblib import pandas as pd from sklearn import tree if __name__ == \"__main__\": parser = argparse.ArgumentParser() # Hyperparameters are described here. In this simple example we are just including one hyperparameter. parser.add_argument(\"--max_leaf_nodes\", type=int, default=-1) # Sagemaker specific arguments. Defaults are set in the environment variables. parser.add_argument(\"--output-data-dir\", type=str, default=os.environ[\"SM_OUTPUT_DATA_DIR\"]) parser.add_argument(\"--model-dir\", type=str, default=os.environ[\"SM_MODEL_DIR\"]) parser.add_argument(\"--train\", type=str, default=os.environ[\"SM_CHANNEL_TRAIN\"]) args = parser.parse_args() # Take the set of files and read them all into a single pandas dataframe input_files = [os.path.join(args.train, file) for file in os.listdir(args.train)] if len(input_files) == 0: raise ValueError( ( \"There are no files in {}.n\" + \"This usually indicates that the channel ({}) was incorrectly specified,n\" + \"the data specification in S3 was incorrectly specified or the role specifiedn\" + \"does not have permission to access the data.\" ).format(args.train, \"train\") ) raw_data = [pd.read_csv(file, header=None, engine=\"python\") for file in input_files] train_data = pd.concat(raw_data) # labels are in the first column train_y = train_data.iloc[:, 0] train_X = train_data.iloc[:, 1:] # Here we support a single hyperparameter, \'max_leaf_nodes\'. Note that you can add as many # as your training my require in the ArgumentParser above. max_leaf_nodes = args.max_leaf_nodes # Now use scikit-learn\'s decision tree classifier to train the model. clf = tree.DecisionTreeClassifier(max_leaf_nodes=max_leaf_nodes) clf = clf.fit(train_X, train_y) # Print the coefficients of the trained classifier, and save the coefficients joblib.dump(clf, os.path.join(args.model_dir, \"model.joblib\")) def model_fn(model_dir): \"\"\"Deserialized and return fitted model Note that this should have the same name as the serialized model in the main method \"\"\" clf = joblib.load(os.path.join(model_dir, \"model.joblib\")) return clf 学習 SageMaker(またはローカル)の.ipynb は、SKLearnインスタンスに対して fit() を実行するだけで良い。 sklearn.fit({\"train\": train_input}) 推論 なんと、推論はWebインターフェースになっている。 推論コンテナ内でnginxが動作し、PythonWebAppが wsgi(gunicorn) を介してnginxから入力/応答する。 SageMaker(またはローカル)の.ipynbからは、SKLearnインスタンスに対してdeploy()を実行する。 推論コンテナへのインターフェースとなるインスタンスが生成され、後はこのインスタンスに対してpredict()を呼ぶ。 推論用にデータを集めて predict()を実行する例。 テストデータと推論の結果を並べて表示している。 うまくいっていれば同じになるはず。 (こんな風に訓練データとテストデータを拾って良いのかはさておき...) predictor = sklearn.deploy(initial_instance_count=1, instance_type=\"local\") import itertools import pandas as pd shape = pd.read_csv(\"data/iris.csv\", header=None) a = [50 * i for i in range(3)] b = [40 + i for i in range(10)] indices = [i + j for i, j in itertools.product(a, b)] test_data = shape.iloc[indices[:-1]] test_X = test_data.iloc[:, 1:] test_y = test_data.iloc[:, 0] print(predictor.predict(test_X.values)) print(test_y.values) /invocationsというURLに対してPOSTリクエストが発行されている。 応答は以下の通り、 テストデータの説明変数と、predict()の結果得られた値が一致していそう。 hqy7i6eoyi-algo-1-vq8df | 2021-05-22 16:26:50,617 INFO - sagemaker-containers - No GPUs detected (normal if no gpus installed) [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 2. 2. 2. 2. 2. 2. 2. 2. 2.]hqy7i6eoyi-algo-1-vq8df | 172.23.0.1 - - [22/May/2021:16:26:51 +0000] \"POST /invocations HTTP/1.1\" 200 360 \"-\" \"python-urllib3/1.26.4\" [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 2. 2. 2. 2. 2. 2. 2. 2. 2.] まとめ SageMaker用の機械学習のHelloWorldをローカルで動かしてみた。 (かなり雑だけども), SageMakerのサンプルコードをちょっと修正するだけでローカルで動くことがわかった。
Laravel8 sailで環境構築
とにかく進歩が早いLaravel。 セマンティックバージョニングになった6あたりから結構な速度で機能を乗せて来た感がある. 付いていくのがなかなか大変というのはある. 開けた口に無理やり食べ物を押し込んでくるような強引さの中にセンスの良さを感じ取れるので、 ちょっと付いて行ってみることにする. [arst_toc tag=\"h4\"] Laravel sail Laravel公式が用意するDocker開発環境を操作する軽量なコマンドラインインターフェース. ポイントは、コンテナの外部からコンテナ内のLaravelに対してコマンドを実行できる点. dockerコマンドをラップし、コンテナの内部で実行した結果を応答する仕組みとなっている. フルスタックフレームワークであるLaravelらしく何でも内包してしまう. composerやartisanコマンド実行のために、わざわざdockerコマンドを叩くのは辛い. sailが無いとdockerコマンドを叩きまくるか、コンテナに入って作業する必要がある. sailを使うことで、コンテナの中に入らず外からかsailコマンドを実行できる. こんな風にするとdockerの上位に来る仕組みを作れるのか、と結構感動. sailでプロジェクトを作る 既存のプロジェクトにsailを導入するパターンと、新規にプロジェクトを作成するパターンの2通りがある. 今回は新規にプロジェクトを作成していく. https://laravel.build/example-app というURLはShellScriptのコードを返す. withの後ろにインストールしたいミドエウウェアを指定する. 今回はmysqlだけ. カンマ区切りで複数指定可. $ mkdir -p ~/hoge && cd ~/hoge $ curl -s \"https://laravel.build/example-app?with=mysql\" | bash $ cd example-app && ./vendor/bin/sail up ちなみに、https://laravel.build/example-appは以下のShellScriptを返す. そのShellScriptは何をやっているかというと. laravelsail/php80-composerというイメージからコンテナを起動する. laravel newコマンドでプロジェクトを作成する. artisan sail:installコマンドを実行する. ディレクトリのOwnerを変更する. (パスワードが要求される) docker info > /dev/null 2>&1 # Ensure that Docker is running... if [ $? -ne 0 ]; then echo \"Docker is not running.\" exit 1 fi docker run --rm -v $(pwd):/opt -w /opt laravelsail/php80-composer:latest bash -c \"laravel new example-app && cd example-app && php ./artisan sail:install --with=mysql\" cd example-app CYAN=\'33[0;36m\' LIGHT_CYAN=\'33[1;36m\' WHITE=\'33[1;37m\' NC=\'33[0m\' echo \"\" if sudo -n true 2>/dev/null; then sudo chown -R $USER: . echo -e \"${WHITE}Get started with:${NC} cd example-app && ./vendor/bin/sail up\" else echo -e \"${WHITE}Please provide your password so we can make some final adjustments to your application\'s permissions.${NC}\" echo \"\" sudo chown -R $USER: . echo \"\" echo -e \"${WHITE}Thank you! We hope you build something incredible. Dive in with:${NC} cd example-app && ./vendor/bin/sail up\" fi sailでコンテナを立ち上げる 要はdocker-compose upをラップしたsail upコマンドを叩く. PHPのbundlerであるcomposerの仕様上, vendor 以下にモジュールがインストールされる. sailコマンドも ./vendor/bin/ に入っている. そこで ./vendor/bin/sail up を実行する. $ cd example-app $ ./vendor/bin/sail up dockerそのものなので, Ctrl+Cで落ちる. もちろん、./vendor/bin/sail up -d によりバックグラウンドで立ち上がる. $ ./vendor/bin/sail up -d ブラウザからhttp://localhostを開く あっさり開けた. ちなみに Dockerfile内で /usr/local/bin/start-containerを実行している. start-container内ではsupervisordによりLaravelのビルトインサーバをデーモン化している. #!/usr/bin/env bash if [ ! -z \"$WWWUSER\" ]; then usermod -u $WWWUSER sail fi if [ ! -d /.composer ]; then mkdir /.composer fi chmod -R ugo+rw /.composer if [ $# -gt 0 ];then exec gosu $WWWUSER \"$@\" else /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf fi supervisord.confは以下の通り. [supervisord] nodaemon=true user=root logfile=/var/log/supervisor/supervisord.log pidfile=/var/run/supervisord.pid [program:php] command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 user=sail environment=LARAVEL_SAIL=\"1\" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 sailでLaravelのバージョンを確認してみる 試しにコンテナの外からsailコマンドでartisan --versionを実行してみる. まるでコンテナの外からartisanコマンドを打っているような感覚. 良いと思う. $ ./vendor/bin/sail artisan --version Laravel Framework 8.41.0
分散と標準偏差を計算しやすくする
[mathjax] 分散と標準偏差を計算しやすく変形できる。 いちいち偏差(x_i-bar{x})を計算しておかなくても、2乗和(x_i^2)と平均(bar{x})がわかっていればOK。 begin{eqnarray} s^2 &=& frac{1}{n} sum_{i=1}^n (x_i - bar{x})^2 \\ &=& frac{1}{n} sum_{i=1}^n ( x_i^2 -2 x_i bar{x} + bar{x}^2 ) \\ &=& frac{1}{n} ( sum_{i=1}^n x_i^2 -2 bar{x} sum_{i=1}^n x_i + bar{x}^2 sum_{i=1}^n 1) \\ &=& frac{1}{n} ( sum_{i=1}^n x_i^2 -2 n bar{x}^2 + nbar{x}^2 ) \\ &=& frac{1}{n} ( sum_{i=1}^n x_i^2 - nbar{x}^2 ) \\ &=& frac{1}{n} sum_{i=1}^n x_i^2 - bar{x}^2 \\ s &=& sqrt{frac{1}{n} sum_{i=1}^n x_i^2 - bar{x}^2 } end{eqnarray} 以下みたい使える。平均と標準偏差と2乗和の関係。 begin{eqnarray} sum_{i=1}^n (x_i - bar{x})^2 &=& sum_{i=1}^n x_i^2 - nbar{x}^2 \\ ns^2 &=& sum_{i=1}^n x_i^2 - nbar{x}^2 \\ sum_{i=1}^n x_i^2 &=& n(s^2 + bar{x} ) end{eqnarray}
ロジスティック回帰
[mathjax] 参考書籍を読んでいく. 今回はロジスティック回帰. 未知のサンプルのクラスの所属確率を見積もることができる, という重要なことが書かれている. また重みの学習は勾配降下法のフレームワークの中で統一されていて一連の理解が進む. 一度は理解しておいた方が良い内容だと思った. パーセプトロンとADALINE 線形和(z=w^T)を考えたとき 「サンプル(x)が決定境界の上にある」 というのは(w^Tx=0). (w^T x > 0), (w^T x < 0) を決定境界の 「こちら側」 か 「向こう側」 か対応させる. パーセプトロンのアイデアは以下の通り. 線形和 (z=w^T x)を決定境界とし, (phi(z)in (-1,+1))を予測ラベルとする 予測が誤った回数, つまり真のラベルと予測ラベルの差の合計がゼロになるように(w)を学習する (phi(z))はステップ関数で連続ではないので解析的な式の変形ができない 決定境界により完全に分離できなければ学習が収束することはない 決定境界から離れている度合いを連続で凸な関数として定義し, その関数が極小となる(w)を求めていこう, というアイデアに拡張したのがADALINE. (phi(z) = z). コスト関数(J(w) = frac{1}{2} sum_i left( hat{y^{(i)} - phi ( z^{(i)})} right)^2 ) トレーニングセット全体から(J(w))の極小化を考えることが可能. (勾配降下法) トレーニングセットの一部を使って(J(w))の極小化を考え, 他のトレーニングセットで反復することも可能. (確率的勾配降下法) ロジスティック回帰 決定境界を表す線形和(z=w^T x)について(phi(z) in (mathbb{R}| 0 leq z leq +1))を考える. 線形和(z)がクラス1に分類される確率を表す(phi(z))を考える. (phi(z))をどう作るのか 線形和を(0)から(1)の実数全体に変換する関数を考える. オッズ比の対数をとったロジット関数(logit(p))を使う. ここで (p)は(x)が与えられたときにサンプルデータがクラス1に属する確率. begin{eqnarray} logit(p(y=1 | x)) &=& log frac{p}{1-p} \\ &=& w_0 x_0 + w_w x_w + cdots + w_m x_m \\ &=& sum_{i=0}^m w_i x_i \\ &=& w^T x end{eqnarray} つまり, 線形和(z = w^T x in (mathbb{R} | 0 leq z leq 1) ) (p)は未知であるということが重要. この式から(p)を予測していく. (f(p) = log frac{p}{1-p}) の逆関数が分かれば良い. begin{eqnarray} f(p) &=& log frac{p}{1-p} \\ e^{f(p)} &=& frac{p}{1-p} \\ (1-p)e^{f(p)} &=& p \\ e^{f(p)} &=& p(1+e^{f(p)}) \\ p &=& frac{e^{f(p)}}{1+e^{f(p)}} \\ p &=& frac{1}{1+e^{-f(p)}} end{eqnarray} (f(p)=z)としていたので以下のようになる. (zとpが混在していてたぶん数式としての表現は間違ってる.. ) begin{eqnarray} p &=& frac{1}{1+e^{-z}} end{eqnarray} (p)は確率なので(p in (mathbb{R}|0 leq p leq 1)). (phi(z) in (mathbb{R}|0 leq p leq 1) )とすると, begin{eqnarray} phi(z) = frac{1}{1+e^{-z}} end{eqnarray} (z-phi(z))をGeoGebraでプロットすると以下のような感じ. (z)を大きくしていくと(phi(z))は限りなく(1)に近づく. (z)を小さくしていくと(phi(z))は限りなく(0)に近づく. (phi(z)=)の出力は正事象が起こる確率であると解釈される. (phi(z)=0.8)である場合, 80%の確率で正事象が起こり,20%の確率で正事象が起こらないことを表す. トレーニングデータからパラメタを学習し, ある(phi(z))が得られたとして, 未知のサンプルのクラスの所属関係を確率で見積もることができる. 天気予報で晴れの確率70%、晴れでない確率30%のように. 予測の際に,クラスの所属関係の確率を見積もることができるのが神がかるレベルで重要!! もし(phi(z))を2値分類に当てはめるのであれば, (phi(z)=0.5)を境に上半分を(1)、下半分を(0)と予測するものとする. begin{eqnarray} hat{y} = begin{cases} 1 & phi(z) ge 0.5 \\ 0 & phi(z) lt 0.5 end{cases} end{eqnarray} それって, (z=0)を境に左半分, 右半分に割り付けるのと同じなので, begin{eqnarray} hat{y} = begin{cases} 1 & z ge 0 \\ 0 & z lt 0 end{cases} end{eqnarray} ロジスティック回帰の重みの学習 では(w)をどうやって学習するか. 式をイジるだけで面倒なだけで,これ以上は洞察得られないなと思うので, 流れだけ書いてみる. 学生時代に形態素解析っぽい何かのアルゴリズムを考えたことがあって, 形態素どうしのベターな接続を求め際に, 接続コストをコーパスから作った確率で表して, 尤度を最大化する問題として解いた記憶があって, 今更合点が行くという謎. 勉強不足で尤度最大化のアイデアがあってそうした訳ではないので, もっと勉強していたらよかったな..と. ADALINEの説明の中で, 真の値と予測値の差の2乗誤差をコスト関数として, 連続で凸な関数の極小化の問題として(w)を求めていった. ロジスティック回帰の場合, 予測は(phi(z))も連続で凸な関数なので, 同じロジックで(w)を求めることもできる. コスト関数(J(w))は同様に以下. begin{eqnarray} J(w) &=& sum_i frac{1}{2} left( phi(z^{(i)}) - y^{(i)} right)^2 end{eqnarray} (phi(z)=frac{1}{1+e^{-z}})だと, これを解析的に微分するのは不可能. トレーニングデータが(m)個あった場合に(i)番目のトレーニングデータにおける確率は以下. (x)も(w)もパラメタですよ,って書き方. begin{eqnarray} P(y|x;w)=P(y^{(i)}|x^{(i)};w) end{eqnarray} 求めたいのは, 全てのトレーニングデータについて(P(y))の積が最大になる(w). 積,つまり尤度を書き下すと以下のようになる. begin{eqnarray} L(w) &=& prod_{i=1}^m left( P(y^{(i)}) | x^{(i)};w right) \\ &=& prod_{i=1}^m left( phi(z^{(i)}) right)^{y^{(i)}} + left( 1-y^{(i)}right) log left( 1-phi(z^{(i)}) right) end{eqnarray} 尤度そのものの最大化するよりも対数をとったものを最大化する方が楽になる. (対数をとると指数の肩が定数倍になり積が和になる. 相当簡単になる. ) begin{eqnarray} l(w) &=& log L(w) \\ &=& sum_{i=1}^{n} left( y^{(i)} log( phi(z^{(i)}) ) + (1-y^{(i)}) log (1-phi(z^{(i)})) right) end{eqnarray} これを最大化するということは, これに(-1)を掛けたものを最小化するということ. そもそもコスト関数(J(w))を立てて最小化しようとしていたので, これを(J(w))と置いてしまう. begin{eqnarray} J(w) = sum_{i=1}^{n} left( -y^{(i)} log( phi(z^{(i)}) ) - (1-y^{(i)}) log (1-phi(z^{(i)})) right) end{eqnarray} ADALINEの勾配降下法で, コスト関数を最小にするように(w)を更新していった. つまり, (w := w - Delta w)という更新をしていった. ロジスティック回帰において, コスト関数を対数尤度で書き直していて, 対数尤度を最大化するように(w)を更新する. つまり, (w := w + Delta w)という更新をおこなう. 対数尤度を最大化する(w)を更新する話は, コスト関数を最小化する(w)を更新する話と同じ, なので, 結局のところ勾配降下法と同じ式となる. begin{eqnarray} Delta w_j &:=& - eta frac{partial J}{partial w_j} end{eqnarray} 尤度関数の偏導関数はシグモイド関数の偏導関数を使って以下のようになるらしい. (省略) シグモイド関数の偏導関数は以下 begin{eqnarray} frac{partial phi(z)}{partial z} &=& frac{1}{partial z} frac{1}{1-e^{-z}} \\ &=& frac{1}{1+e^{-1}} left( 1- frac{1}{1+e^{-z}} right) \\ &=& phi(z) (1-phi(z)) end{eqnarray} 対数尤度の偏導関数はシグモイド関数の偏導関数を使って以下. begin{eqnarray} frac{partial l(w)}{partial w_j} &=& left( y frac{1}{phi(z)} - (1-y) frac{1}{1-phi(z)} right) frac{partial}{partial w_j} phi(z) \\ &=& left( yfrac{1}{phi(z)} - (1-y) frac{1}{1-phi(z)} right) phi(z)(1-phi(z)) frac{partial}{partial w_j} z \\ &=& left( y(1-phi(z))-(1-y)phi(z) right) x_j \\ &=& left( y-phi(z) right) x_i end{eqnarray} コスト関数の最小化と対数尤度関数の最大化が同じ,というロジックが効いて, ADALINEの更新と全く同じになる. begin{eqnarray} w_j &:=& - eta frac{partial J}{partial w_j} \\ &=& eta sum_{i=1}^n left( y^{(i)} -phi(z^{(i)}) right) x_j^{(i)} end{eqnarray} 奇跡的..
機械学習の分類問題と損失関数の最小化の話
[mathjax] 参考にした書籍でこの順序で誘導されていて理解しやすかったです. パーセプトロンによる一番簡単な教師あり学習を理解する ADALINEにより学習を凸で連続なコスト関数の最小化問題として捉える パーセプトロンの学習 2値のラベル((+1),(-1))が付与されているサンプルデータが与えられたとする. ラベル値が(-1)であるデータと, ラベル値が(+1)であるデータに分類できる. それらの境界が線形和で表される問題である場合, その境界を求めることができれば, 未知のデータに対してその境界の(+1)側か(-1)側かを判定できる. (m)次のサンプルデータと重みの線形和を考える. [ boldsymbol{z} = boldsymbol{w}^T boldsymbol{x} = w_1 x_1 + w_2 x_2 + cdots x_m x_m ] (z)に関するステップ関数を用意する. こうすることで (z)-(phi(z)) 空間において(z=theta)を境に線形和(z)が(-1),(+1)に分かれることを表現できる. [ varphi(z) = begin{cases} 1 & (z gt theta) \\ -1 & (z leq theta) end{cases} ] (theta)を左辺に移動し, (x_0 = 1), (w_0 = -theta) とすると, 線形和に(theta)の項を組み入れることができる. 新しい線形和(z\')が(0)になる箇所を境にステップ関数の応答値が切り替わる. [ varphi(z) = begin{cases} 1 & (z-theta gt 0) \\ -1 & (z-theta leq 0) end{cases} ] [ z\' = z - theta = w_0 x_0 + w_1 x_1 + w_2 x_2 + cdots x_m x_m ] あるパラメータ(boldsymbol{w})が存在するとして, データ(boldsymbol{x})について, (boldsymbol{w}^T boldsymbol{x}=0)を境界にステップ関数の応答値が切り替わる. 未知のデータ(x)に対して, (boldsymbol{w}^T boldsymbol{x})はラベルの予測値(+1), (-1)を応答する. 予測値(varphi(w^Tx))が正解であるサンプルデータに含まれる(y^{i})と同じとなる(w)を探したい. それを探していく. 最初,(w)に初期値を設定し, 以下の手続きによって(w)を更新していく. 上付きの添字はサンプルデータ内の順序数を表している. (y^{(i)})は(i)番目のサンプルデータ. 右下の添字は該当サンプルデータの各次元を表している. (y^{(i)_m})は(y^{(i)})の(m)番目の要素. 現在の(w)を使って予測値を計算する. (hat{y}=w^T x) (w)を更新する. (Delta w_j := eta(y^{i}-hat{y})x^{i}_j) 1個のサンプルデータで1回(w)が更新される. そもそもデータセットを綺麗に線形和が表す決定境界で分離できないような状態だと、 何度やっても予測したクラスラベルと真のクラスラベルの差が埋まらない。 あっちを立てればこっちが立たず、みたいになる。 予測したクラスラベルと真のクラスラベルを比較するこの方法だと、 (Delta w_j := eta(y^{i}-hat{y})x^{i}_j) が良い値なのか悪い値なのか、繰り返さないとわからない。 予測と真の値の比較を凸で連続な関数で表すことができれば、その関数の凸の底に向かうやり方で、 「今より良い次の値」に更新する方法を解析的に求めることができて都合が良い。 真の値と予測した値の差が「損失」とか「コスト」とか呼んで、 「コスト関数」を最小化するパラメータが良いパラメータなんだろう、という話が進んでいく。 ADALINEの学習 線形和をステップ関数に変換する部分があった。 こうして(w^T x varphi(w^T x))空間で (w^T x)が(-1),(+1)に分かれる(w)を求めようとした. [ varphi(z) = begin{cases} 1 & (z-theta gt 0) \\ -1 & (z-theta leq 0) end{cases} ] そうではなく以下の恒等式(左辺と右辺が同じ)を使い,真の値と予測した値の差の求め方を変えると, 差が凸で連続な関数で表せるようになり (解析的に微分できるようになり), 一番小さいところは? という話がしやすくなる. [ varphi(z)=z ] 最小二乗法のときやったやつで、差の2乗を足し合わると符号がキャンセルされてよくて、差を式で表せる。 [ J(w) = sum_i left( y^{(i)} - varphi(w^T x^{(i)}) right)^2 ] こういうのを誤差平方和と言うようで(frac{1}{2})倍するらしい. [ J(w) =frac{1}{2} sum_i left( y^{(i)} - varphi(w^T x^{(i)}) right)^2 ] (J(w))がなんで連続で凸なのかは知ったことではないが、 連続で凸であれば(J\'(w)=0)を解くと極小となる(w)が求まるのは高校生のときにやった話. (J(w))の接線(多次元だと接っする面)の傾きが(J\'(w))。 ただ(w)には次数があって傾きは以下(それぞれの次数毎の極限). [ nabla J(w) = frac{partial J(w)}{partial w_j} ] (w J(w))空間で(nabla J(w))は(w)における傾きなので, (w)に(-nabla J(w) cdot 1)を足すと, (J(w))が極小になる箇所に近づく. どれだけ行けば良いかわからないので(-nabla J(w) cdot eta)を足すことにする. (w)を以下の通り更新する. begin{eqnarray} w &:=& w + nabla J(w) \\ &:=& w -nabla J(w) cdot eta end{eqnarray} ちなみに(nabla J(w))は式をこねくり回すと計算できる. 最終的に更新は以下の通りとなる. [ w := w - eta sum_i left( y^{(i)} -varphi left( z^{(i)} right) right) x_j^{(i)} ] パーセプトロンの更新とそっくりな形が出てきて感動する... あっちは(varphi(z))が不連続なステップ関数でこっちは連続な関数. 式をこねくり回す際に, (varphi(z) = z)の恒等式の関係を使っていない. 何か連続な関数であればこれが成り立つ. 形式上,例えば(varphi(z))を以下(シグモイド)とすることでロジスティック回帰 (本当か? 次回確認.). [ varphi (z) = frac{1}{1+e^{-z}} ]
Grafanaプラグインを読んでいく – Clock plugin
最も単純そうなプラグインを読んでいくシリーズ。 プラグインは Clock plugin。Panelプラグイン。配布はここ。 最も単純そうなDataSourceプラグインを読む記事は以下。 [clink url=\"https://ikuty.com/2020/11/14/grafana-code-read/\"] ダッシュボードに時計を表示できる。 ダッシュボードから設定をおこない表示に反映する機能を備えていて、 PanelプラグインのHelloWorldには良い感じ。 The Clock Panel can show the current time or a countdown and updates every second. Show the time in another office or show a countdown to an important event. 肝心のデータプロットに関する機能は無いので別途違うコンポーネントを読む。 インストール、ビルド 公式からインストールすると src が含まれない。 ソースコードをclone、buildすることにする。 初回だけgrafana-serverのrestartが必要。 # clone repository $ cd ~/ $ git clone https://github.com/grafana/clock-panel.git $ mv clock-panek /var/lib/grafana/plugins # install plugin $ yarn install $ yarn build # restart grafana-server $ sudo service grafana-server restart ディレクトリ・ファイル構成 ディレクトリ・ファイル構成は以下の通り。 clock-panel/ src/ ClockPanel.tsx ... プラグイン本体 module.ts ... プラグインのエントリポイント options.tsx plugin.json ... プラグインの設定ファイル types.ts ... TypeScript型定義 img/ ... 画像リソース clock.svg countdown1.png screenshot-clock-options.png screenshot-clocks.png screenshot-showcase.png external/ ... 外部ライブラリ moment-duration-formant.js エントリポイント ./module.ts の内容は以下の通り。 ClockPanel.tsxで定義済みのClockPanelクラスをexportしている。 options.tsxに記述したオプション画面関連のクラスを.setPanelOptions()を介して設定する。 import { PanelPlugin } from \'@grafana/data\'; import { ClockPanel } from \'./ClockPanel\'; import { ClockOptions } from \'./types\'; import { optionsBuilder } from \'./options\'; export const plugin = new PanelPlugin(ClockPanel).setNoPadding().setPanelOptions(optionsBuilder); 本体 (ClockPanel.tsx) ./ClockPanel.tsxを読んでいく。React+TypeScript。 import React, { PureComponent } from \'react\'; import { PanelProps } from \'@grafana/data\'; import { ClockOptions, ClockType, ZoneFormat, ClockMode } from \'./types\'; import { css } from \'emotion\'; // eslint-disable-next-line import moment, { Moment } from \'moment\'; import \'./external/moment-duration-format\'; interface Props extends PanelProps {} interface State { // eslint-disable-next-line now: Moment; } export function getTimeZoneNames(): string[] { return (moment as any).tz.names(); } // PureComponentクラスを派生させることでプラグイン用のパネルクラスを定義できる。 // パネルのプロパティは PanelProps型だが 当プラグイン用にProps型に拡張している。 export class ClockPanel extends PureComponent { timerID?: any; state = { now: this.getTZ(), timezone: \'\' }; //Componentのインスタンスが生成されDOMに挿入されるときに呼ばれる //DOM挿入後,1秒間隔で this.tick()の実行を開始する。 componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 // 1 second ); } //[非推奨] DOMから削除されるときに呼ばれる。古いのかな。 //this.tick()の実行を停止する。 componentWillUnmount() { clearInterval(this.timerID); } //DOM挿入後1秒間隔で呼ばれる。 //stateを更新する。 tick() { const { timezone } = this.props.options; this.setState({ now: this.getTZ(timezone) }); } //時刻フォーマットを取得する。 //時刻フォーマットはオプション設定画面で設定され props.optionsに渡される。 //渡される変数は clockType と timeSettings である。 clockTypeは 12時間/24時間のいずれか。 //12時間なら h:mm:ss A, 24時間なら HH:mm:ss。 //カスタムの場合,clockTypeがClockType.Customになる。 getTimeFormat() { const { clockType, timeSettings } = this.props.options; if (clockType === ClockType.Custom && timeSettings.customFormat) { return timeSettings.customFormat; } if (clockType === ClockType.H12) { return \'h:mm:ss A\'; } return \'HH:mm:ss\'; } // Return a new moment instnce in the selected timezone // eslint-disable-next-line getTZ(tz?: string): Moment { if (!tz) { tz = (moment as any).tz.guess(); } return (moment() as any).tz(tz); } //カウントダウン文字列を得る //設定値countdownSettings, timezone は props.options から得られる。 // getCountdownText(): string { const { now } = this.state; const { countdownSettings, timezone } = this.props.options; //カウントダウン終了時 設定された文字列 countdownSettings.endText を返す if (!countdownSettings.endCountdownTime) { return countdownSettings.endText; } //残り時間を計算。 const timeLeft = moment.duration( moment(countdownSettings.endCountdownTime) .utcOffset(this.getTZ(timezone).format(\'Z\'), true) .diff(now) ); let formattedTimeLeft = \'\'; //計算した残り時間が0以下であれば、設定された文字列 countdownSettings.endText を返す。 if (timeLeft.asSeconds() 0) { formattedTimeLeft = timeLeft.years() === 1 ? \'1 year, \' : timeLeft.years() + \' years, \'; previous = \'years\'; } // Y months (or Y month) if (timeLeft.months() > 0 || previous === \'years\') { formattedTimeLeft += timeLeft.months() === 1 ? \'1 month, \' : timeLeft.months() + \' months, \'; previous = \'months\'; } // Z days (or Z day) if (timeLeft.days() > 0 || previous === \'months\') { formattedTimeLeft += timeLeft.days() === 1 ? \'1 day, \' : timeLeft.days() + \' days, \'; previous = \'days\'; } // A hours (or A hour) if (timeLeft.hours() > 0 || previous === \'days\') { formattedTimeLeft += timeLeft.hours() === 1 ? \'1 hour, \' : timeLeft.hours() + \' hours, \'; previous = \'hours\'; } // B minutes (or B minute) if (timeLeft.minutes() > 0 || previous === \'hours\') { formattedTimeLeft += timeLeft.minutes() === 1 ? \'1 minute, \' : timeLeft.minutes() + \' minutes, \'; } // C minutes (or C minute) formattedTimeLeft += timeLeft.seconds() === 1 ? \'1 second \' : timeLeft.seconds() + \' seconds\'; return formattedTimeLeft; } //Zoneを表示するh4タグを作成して返す。Reactっぽい。 renderZone() { const { now } = this.state; const { timezoneSettings } = this.props.options; const { zoneFormat } = timezoneSettings; // ReactでCSSを書く作法。ヒアドキュメント // ロジックコードの中にHTML生成コードが混在して非常に見辛い。 const clazz = css` font-size: ${timezoneSettings.fontSize}; font-weight: ${timezoneSettings.fontWeight}; line-height: 1.4; `; let zone = this.props.options.timezone || \'\'; switch (zoneFormat) { case ZoneFormat.offsetAbbv: zone = now.format(\'Z z\'); break; case ZoneFormat.offset: zone = now.format(\'Z\'); break; case ZoneFormat.abbv: zone = now.format(\'z\'); break; default: try { zone = (this.getTZ(zone) as any)._z.name; } catch (e) { console.log(\'Error getting timezone\', e); } } return ( {zone} {zoneFormat === ZoneFormat.nameOffset && ( ({now.format(\'Z z\')}) > )} </h4> ); } //Dateを表示するh3タグを返す。 renderDate() { const { now } = this.state; const { dateSettings } = this.props.options; const clazz = css` font-size: ${dateSettings.fontSize}; font-weight: ${dateSettings.fontWeight}; `; const disp = now.locale(dateSettings.locale || \'\').format(dateSettings.dateFormat); return ( {disp} ); } //Timeを返すh2タグを返す。 renderTime() { const { now } = this.state; const { timeSettings, mode } = this.props.options; const clazz = css` font-size: ${timeSettings.fontSize}; font-weight: ${timeSettings.fontWeight}; `; const disp = mode === ClockMode.countdown ? this.getCountdownText() : now.format(this.getTimeFormat()); return {disp}; } //React componentとしてrender()メソッドを実装する必要がある。 //CSSを整形してZone,Date,Timeを設定して返す。 render() { const { options, width, height } = this.props; const { bgColor, dateSettings, timezoneSettings } = options; const clazz = css` display: flex; align-items: center; justify-content: center; flex-direction: column; background-color: ${bgColor ?? \'\'}; text-align: center; `; return ( {dateSettings.showDate && this.renderDate()} {this.renderTime()} {timezoneSettings.showTimezone && this.renderZone()} ); } } 設定 (options.tsx) 設定画面側を読んでいく。 PanelOptionsEditorBuilder型の引数を取り、builderに対して機能実装していく。 機能実装というのは、つまり、ラジオボタンを追加したり、カスタムエディタを追加したり、など。 この実装で以下のような設定画面が表示される。(README.mdは古いので注意)。 Modeとして、時間をそのまま表示するTimeモードか、カウンドダウンモードかを二者択一で設定する。 背景色(BackgroundColor)をカラーピッカーで設定する。(ちなみにGrafanaV7では機能しない様子)。 addTimeFormat()メソッドにより、24h表示/12h表示/カスタム表示,FontSize,FontWeightの設定機能を追加する。 addTimeZone()メソッドにより,TimeZoneと表示有無の設定機能を追加する。 カウントダウンモードに設定すると、カウントダウン設定をおこなえるが, addCountdown()メソッドにより,カウントダウン設定を追加する。 決められた構文にしたがって欲しい機能を追加していくだけなので、 設定画面の実装が必要になったら必要な構文を調べて追加していくことになりそう。 import React from \'react\'; import { PanelOptionsEditorBuilder, GrafanaTheme, dateTime } from \'@grafana/data\'; import { ColorPicker, Input, Icon, stylesFactory } from \'@grafana/ui\'; import { css } from \'emotion\'; import { config } from \'@grafana/runtime\'; import { ClockOptions, ClockMode, ClockType, FontWeight, ZoneFormat } from \'./types\'; import { getTimeZoneNames } from \'./ClockPanel\'; export const optionsBuilder = (builder: PanelOptionsEditorBuilder) => { // Global options builder //ClockModeの二者択一。TimeかCountdownを選ばせる。 .addRadio({ path: \'mode\', name: \'Mode\', settings: { options: [ { value: ClockMode.time, label: \'Time\' }, { value: ClockMode.countdown, label: \'Countdown\' }, ], }, defaultValue: ClockMode.time, }) //背景色のカスタムエディタ。カラーピッカーから色を選ばせる。 .addCustomEditor({ id: \'bgColor\', path: \'bgColor\', name: \'Background Color\', editor: props => { const styles = getStyles(config.theme); let prefix: React.ReactNode = null; let suffix: React.ReactNode = null; if (props.value) { suffix = props.onChange(undefined)} />; } prefix = ( ); return ( { console.log(\'CLICK\'); }} prefix={prefix} suffix={suffix} /> ); }, defaultValue: \'\', }); // TODO: refreshSettings.syncWithDashboard addCountdown(builder); addTimeFormat(builder); addTimeZone(builder); addDateFormat(builder); }; //--------------------------------------------------------------------- // COUNTDOWN //--------------------------------------------------------------------- function addCountdown(builder: PanelOptionsEditorBuilder) { const category = [\'Countdown\']; builder .addTextInput({ category, path: \'countdownSettings.endCountdownTime\', name: \'End Time\', settings: { placeholder: \'ISO 8601 or RFC 2822 Date time\', }, defaultValue: dateTime(Date.now()) .add(6, \'h\') .format(), showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.endText\', name: \'End Text\', defaultValue: \'00:00:00\', showIf: o => o.mode === ClockMode.countdown, }) .addTextInput({ category, path: \'countdownSettings.customFormat\', name: \'Custom format\', settings: { placeholder: \'optional\', }, defaultValue: undefined, showIf: o => o.mode === ClockMode.countdown, }); } //--------------------------------------------------------------------- // TIME FORMAT //--------------------------------------------------------------------- function addTimeFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Time Format\']; builder .addRadio({ category, path: \'clockType\', name: \'Clock Type\', settings: { options: [ { value: ClockType.H24, label: \'24 Hour\' }, { value: ClockType.H12, label: \'12 Hour\' }, { value: ClockType.Custom, label: \'Custom\' }, ], }, defaultValue: ClockType.H24, }) .addTextInput({ category, path: \'timeSettings.customFormat\', name: \'Time Format\', description: \'the date formatting pattern\', settings: { placeholder: \'date format\', }, defaultValue: undefined, showIf: opts => opts.clockType === ClockType.Custom, }) .addTextInput({ category, path: \'timeSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'12px\', }) .addRadio({ category, path: \'timeSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, }); } //--------------------------------------------------------------------- // TIMEZONE //--------------------------------------------------------------------- function addTimeZone(builder: PanelOptionsEditorBuilder) { const category = [\'Timezone\']; const timezones = getTimeZoneNames().map(n => { return { label: n, value: n }; }); timezones.unshift({ label: \'Default\', value: \'\' }); builder .addSelect({ category, path: \'timezone\', name: \'Timezone\', settings: { options: timezones, }, defaultValue: \'\', }) .addBooleanSwitch({ category, path: \'timezoneSettings.showTimezone\', name: \'Show Timezone\', defaultValue: false, }) .addSelect({ category, path: \'timezoneSettings.zoneFormat\', name: \'Display Format\', settings: { options: [ { value: ZoneFormat.name, label: \'Normal\' }, { value: ZoneFormat.nameOffset, label: \'Name + Offset\' }, { value: ZoneFormat.offsetAbbv, label: \'Offset + Abbreviation\' }, { value: ZoneFormat.offset, label: \'Offset\' }, { value: ZoneFormat.abbv, label: \'Abbriviation\' }, ], }, defaultValue: ZoneFormat.offsetAbbv, showIf: s => s.timezoneSettings?.showTimezone, }) .addTextInput({ category, path: \'timezoneSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'font size\', }, defaultValue: \'12px\', showIf: s => s.timezoneSettings?.showTimezone, }) .addRadio({ category, path: \'timezoneSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.timezoneSettings?.showTimezone, }); } //--------------------------------------------------------------------- // DATE FORMAT //--------------------------------------------------------------------- function addDateFormat(builder: PanelOptionsEditorBuilder) { const category = [\'Date Options\']; builder .addBooleanSwitch({ category, path: \'dateSettings.showDate\', name: \'Show Date\', defaultValue: false, }) .addTextInput({ category, path: \'dateSettings.dateFormat\', name: \'Date Format\', settings: { placeholder: \'Enter date format\', }, defaultValue: \'YYYY-MM-DD\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.locale\', name: \'Locale\', settings: { placeholder: \'Enter locale: de, fr, es, ... (default: en)\', }, defaultValue: \'\', showIf: s => s.dateSettings?.showDate, }) .addTextInput({ category, path: \'dateSettings.fontSize\', name: \'Font size\', settings: { placeholder: \'date format\', }, defaultValue: \'20px\', showIf: s => s.dateSettings?.showDate, }) .addRadio({ category, path: \'dateSettings.fontWeight\', name: \'Font weight\', settings: { options: [ { value: FontWeight.normal, label: \'Normal\' }, { value: FontWeight.bold, label: \'Bold\' }, ], }, defaultValue: FontWeight.normal, showIf: s => s.dateSettings?.showDate, }); } const getStyles = stylesFactory((theme: GrafanaTheme) => { return { colorPicker: css` padding: 0 ${theme.spacing.sm}; `, inputPrefix: css` display: flex; align-items: center; `, trashIcon: css` color: ${theme.colors.textWeak}; cursor: pointer; &:hover { color: ${theme.colors.text}; } `, }; });
Grafanaプラグインを読んでいく – simpod-json-datasource plugin
最も単純そうなDataSourceプラグインを読んでいく。 プラグインは simpod-json-datasource plugin。 配布はここ。 JSONを返す任意のURLにリクエストを投げて結果を利用できるようにする。 TypeScriptで書かれている。 The JSON Datasource executes JSON requests against arbitrary backends. JSON Datasource is built on top of the Simple JSON Datasource. It has refactored code, additional features and active development. install インストール方法は以下の通り。 初回だけgrafana-serverのrestartが必要。 # install plugin $ grafana-cli plugins install simpod-json-datasource # restart grafana-server $ sudo service grafana-server restart build ビルド方法は以下の通り。 yarn一発。 # build $ cd /var/lib/grafana/plugins/simpod-json-datasource $ yarn install $ yarn run build 設定 データソースの追加で、今回インストールした DataSource/JSON (simpod-json-datasource)を 選択すると、プラグインのコンフィグを変更できる。 要求するURL一覧 URLはBaseURL。 このプラグインはURLの下にいくつかのURLが存在することを想定している。 つまり、それぞれのURLに必要な機能を実装することでプラグインが機能する。 用語については順に解説する。 必須 Method Path Memo GET / ConfigページでStatusCheckを行うためのURL。200が返ればConfigページで「正常」となる。 POST /search 呼び出し時に「利用可能なメトリクス」を返す。 POST /query 「メトリクスに基づくデータポイント」を返す。 POST /annotations 「アノテーション」を返す。 オプショナル Method Path Memo POST /tag-keys 「ad-hocフィルタ」用のタグのキーを返す。 POST /tag-values 「ad-hocフィルタ」用のタグの値を返す。 メトリクス Grafanaにおいて、整理されていないデータの中から\"ある観点\"に従ったデータを取得したいという ユースケースをモデル化している. 例えば、サーバ群の負荷状況を監視したいというケースでは「サーバ名」毎にデータを取得したいし、 センサ群から得られるデータを監視したいというケースでは「センサID」毎にデータを取得したい. これらの観点は、Grafanaにおいて「メトリクス」または「タグ」として扱われる。 センサAからセンサZのうちセンサPとセンサQのデータのみ表示したい、という感じで使う。 ここで現れるセンサAからセンサZが「メトリクス」である。 クエリ変数のサポート (metricFindQuery) 「クエリ変数」は、「メトリクス」を格納する変数である。 データソースプラグインがクエリ変数をサポートするためには、 DataSourceApi クラスの metricFindQuery を オーバーライド する. string型のqueryパラメタを受け取り、MetricFindValue型変数の配列を返す. 要は以下の問答を定義するものである。 - 質問 : 「どんなメトリクスがありますか?」 - 回答 : 「存在するメトリクスはxxx,yyy,zzzです」 ちなみに、metricFindQueryが受け取るqueryの型は、 metricFindQueryを呼び出すUIがstring型で呼び出すからstring型なのであって、 UIを変更することで別の型に変更することができる。 以下、simpod-json-datasource の metricFindQuery の実装。 // MetricFindValue interfaceはtext,valueプロパティを持つ // { \"label\":\"upper_25\", \"value\": 1}, { \"label\":\"upper_50\", \"value\": 2} のような配列を返す. metricFindQuery(query: string, options?: any, type?: string): Promise { // Grafanaが用意する interpolation(補間)関数 // query として \"query field value\" が渡されたとする // interpolated は { \"target\": \"query field value\" }のようになる. const interpolated = { type, target: getTemplateSrv().replace(query, undefined, \'regex\'), }; return this.doRequest({ url: `${this.url}/search`, data: interpolated, method: \'POST\', }).then(this.mapToTextValue); } // /searchから返ったJSONは2パターンある // 配列 ([\"upper_25\",\"upper_50\",\"upper_75\",\"upper_90\",\"upper_95\"])か、 // map ([ { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} ]) // これらを MetricFindValue型に変換する mapToTextValue(result: any) { return result.data.map((d: any, i: any) => { // mapの場合 if (d && d.text && d.value) { return { text: d.text, value: d.value }; } // 配列の場合 if (isObject(d)) { return { text: d, value: i }; } return { text: d, value: d }; }); } 以下、simpod-json-datasource が メトリクスを取得する部分のUI。 FormatAs, Metric, Additional JSON Dataの各項目が選択可能で、 各々の変数がViewModelとバインドされている. コードは以下。React。 FormatAsのセレクトボックスのOnChangeでsetFormatAs()が呼ばれる。 MetricのセレクトボックスのOnChagneでsetMetric()が呼ばれる。 Additional JSON DataのonBlurでsetData()が呼ばれる。 import { QueryEditorProps, SelectableValue } from \'@grafana/data\'; import { AsyncSelect, CodeEditor, Label, Select } from \'@grafana/ui\'; import { find } from \'lodash\'; import React, { ComponentType } from \'react\'; import { DataSource } from \'./DataSource\'; import { Format } from \'./format\'; import { GenericOptions, GrafanaQuery } from \'./types\'; type Props = QueryEditorProps; const formatAsOptions = [ { label: \'Time series\', value: Format.Timeseries }, { label: \'Table\', value: Format.Table }, ]; export const QueryEditor: ComponentType = ({ datasource, onChange, onRunQuery, query }) => { const [formatAs, setFormatAs] = React.useState<selectableValue>( find(formatAsOptions, option => option.value === query.type) ?? formatAsOptions[0] ); const [metric, setMetric] = React.useState<selectableValue>(); const [data, setData] = React.useState(query.data ?? \'\'); // 第2引数は依存する値の配列 (data, formatAs, metric). // 第2引数の値が変わるたびに第1引数の関数が実行される. React.useEffect(() => { // formatAs.value が 空なら何もしない if (formatAs.value === undefined) { return; } // metric.value が 空なら何もしない if (metric?.value === undefined) { return; } // onChange(..)を実行する onChange({ ...query, data: data, target: metric.value, type: formatAs.value }); onRunQuery(); }, [data, formatAs, metric]); // Metricを表示するセレクトボックスの値に関数がバインドされている. // 関数はstring型のパラメタを1個とる. const loadMetrics = (searchQuery: string) => { // datasourceオブジェクトの metricFindQuery()を呼び出す. // 引数はsearchQuery. 戻り値はMetricFindValue型の配列(key/valueが入っている). // 例えば { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} return datasource.metricFindQuery(searchQuery).then( result => { // { \"text\": \"upper_25\", \"value\": 1}, { \"text\": \"upper_75\", \"value\": 2} から // { \"label\": \"upper_25\", \"value\": 1}, { \"label\": \"upper_75\", \"value\": 2} へ const metrics = result.map(value => ({ label: value.text, value: value.value })); // セレクトボックスで選択中のMetricを取得し setMetric に渡す setMetric(find(metrics, metric => metric.value === query.target)); return metrics; }, response => { throw new Error(response.statusText); } ); }; return ( <> <div className=\"gf-form-inline\"> <div className=\"gf-form\"> <select prefix=\"Format As: \" options={formatAsOptions} defaultValue={formatAs} onChange={v => { setFormatAs(v); }} /> </div> <div className=\"gf-form\"> <asyncSelect prefix=\"Metric: \" loadOptions={loadMetrics} defaultOptions placeholder=\"Select metric\" allowCustomValue value={metric} onChange={v => { setMetric(v); }} /> </div> </div> <div className=\"gf-form gf-form--alt\"> <div className=\"gf-form-label\"> <label>Additional JSON Data </div> <div className=\"gf-form\"> <codeEditor width=\"500px\" height=\"100px\" language=\"json\" showLineNumbers={true} showMiniMap={data.length > 100} value={data} onBlur={value => setData(value)} /> </div> </div> </> ); // } }; クエリ変数から値を取得 (queryの実装) 続いて、選択したメトリクスのデータ列を得るための仕組み. query()インターフェースを実装する. QueryRequest型の引数を受け取る. 引数には、どのメトリクスを使う、だとか、取得範囲だとか、様々な情報が入っている。 QueryRequest型の引数を加工した値を、/query URLにPOSTで渡す. /query URLから戻ってきた値は DataQueryResponse型と互換性があり、query()の戻り値として返る. // UIから送られてきたQueryRequest型の変数optionsを処理して /query に投げるJSONを作る。 // QueryRequest型は当プラグインがGrafanaQuery型interfaceを派生させて作った型。 query(options: QueryRequest): Promise { const request = this.processTargets(options); // 処理した結果、targets配列が空なら空を返す。 if (request.targets.length === 0) { return Promise.resolve({ data: [] }); } // JSONにadhocFiltersを追加する // @ts-ignore request.adhocFilters = getTemplateSrv().getAdhocFilters(this.name); // JSONにscopedVarsを追加する options.scopedVars = { ...this.getVariables(), ...options.scopedVars }; // /queryにJSONをPOSTで投げて応答をDataQueryResponse型で返す。 return this.doRequest({ url: `${this.url}/query`, data: request, method: \'POST\', }); } // UIから送られてきたQueryRequest型変数optionsのtargetsプロパティを加工して返す // processTargets(options: QueryRequest) { options.targets = options.targets .filter(target => { // remove placeholder targets return target.target !== undefined; }) .map(target => { if (target.data.trim() !== \'\') { // JSON様の文字列target.data をJSONオブジェクト(key-value)に変換する // reviverとして関数が指定されている // valueが文字列であった場合にvalue内に含まれる変数名を置換する // 置換ルールは cleanMatch メソッド target.data = JSON.parse(target.data, (key, value) => { if (typeof value === \'string\') { return value.replace((getTemplateSrv() as any).regex, match => this.cleanMatch(match, options)); } return value; }); } // target.targetには、変数のプレースホルダ($..)が存在する. // grafanaユーザが入力した変数がoptions.scopedVarsに届くので、 // target.target内のプレースホルダをoptions.scopedVarsで置換する。 // 置換後の書式は正規表現(regex) if (typeof target.target === \'string\') { target.target = getTemplateSrv().replace(target.target.toString(), options.scopedVars, \'regex\'); } return target; }); return options; } // cleanMatch cleanMatch(match: string, options: any) { // const replacedMatch = getTemplateSrv().replace(match, options.scopedVars, \'json\'); if ( typeof replacedMatch === \'string\' && replacedMatch[0] === \'\"\' && replacedMatch[replacedMatch.length - 1] === \'\"\' ) { return JSON.parse(replacedMatch); } return replacedMatch; } TypeScriptに明るくない場合、以下を参照。 - Typescript-array-filter - 【JavaScript】map関数を用いたおしゃれな配列処理 - TypeScript - String replace() JavaScriptのreplaceとGrafanaのreplaceが混在していて、 IDEが無いとかなり厳しい感じ。 - Interpolate variables in data source plugins - Advanced variable format options query()のパラメタについて、 Grafanaのデフォだとstring型で来るのだが、プラグインが必要に応じて好きに変更できる。 つまりquery()に値を投げる部分をプラグインが変更できるため、変更に応じて受け側も変更する。 当プラグインはDataQueryRequestインターフェースを派生させた型を用意している。 - DataQueryRequest interface getTemplateSrv()はGrafanaのユーティリティ関数。 - getTemplateSrv variable - Github getTemplateSrv - Variable syntax 現在アクティブなダッシュボード内の変数が全て得られる。 * Via the TemplateSrv consumers get access to all the available template variables * that can be used within the current active dashboard. import { getTemplateSrv } from ‘@grafana/runtime’; const templateSrv = getTemplateSrv(); const variablesProtected = templateSrv.getVariables(); const variablesStringfied = JSON.stringify( variablesProtected ); const variables = JSON.parse( variablesStringfied ); URLに投げるJSONは例えば以下の通り。 { \"app\": \"dashboard\", \"requestId\": \"Q171\", \"timezone\": \"browser\", \"panelId\": 23763571993, \"dashboardId\": 1, \"range\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\", \"raw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" } }, \"timeInfo\": \"\", \"interval\": \"50ms\", \"intervalMs\": 50, \"targets\": [ { \"refId\": \"A\", \"data\": \"\", \"target\": \"upper_50\", \"type\": \"timeseries\", \"datasource\": \"JSON-ikuty\" } ], \"maxDataPoints\": 1058, \"scopedVars\": { \"variable1\": { \"text\": [ \"upper_50\" ], \"value\": [ \"upper_50\" ] }, \"__interval\": { \"text\": \"50ms\", \"value\": \"50ms\" }, \"__interval_ms\": { \"text\": \"50\", \"value\": 50 } }, \"startTime\": 1605108668062, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:00.000Z\", \"to\": \"2015-12-22T03:17:00.000Z\" }, \"adhocFilters\": [] } URLから返る値は例えば以下の通り. [ { \"target\": \"pps in\", \"datapoints\": [ [ 622, 1450754160000 ], [ 365, 1450754220000 ] ] }, { \"target\": \"pps out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors out\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] }, { \"target\": \"errors in\", \"datapoints\": [ [ 861, 1450754160000 ], [ 767, 1450754220000 ] ] } ] アノテーションのサポート (annotationQueryの実装) Grafanaには、ユーザがグラフの中に注意を喚起するラベル(またはラベルの範囲)を 設定する機能がある。 ユーザが自力で書くだけでなく、プラグインがアノテーションを提供することもできる。 simpod-json-datasourceプラグインは、アノテーションを提供する機能を有する。 公式の説明はこちら。 例えば下図で薄く赤くなっている部分が プラグインによるアノテーション。 実装方法は以下。 - アノテーションサポートを有効にする - annotationQueryインターフェースを実装する - アノテーションイベントを作成する アノテーションサポートを有効にするためには、 plugin.json に以下を追加する。 { \"annotations\": true } simpod-json-datasourceプラグインのannotationQueryの実装は以下。 annotationQuery( options: AnnotationQueryRequest ): Promise { const query = getTemplateSrv().replace(options.annotation.query, {}, \'glob\'); const annotationQuery = { annotation: { query, name: options.annotation.name, datasource: options.annotation.datasource, enable: options.annotation.enable, iconColor: options.annotation.iconColor, }, range: options.range, rangeRaw: options.rangeRaw, variables: this.getVariables(), }; return this.doRequest({ url: `${this.url}/annotations`, method: \'POST\', data: annotationQuery, }).then((result: any) => { return result.data; }); } プラグインからURLにPOSTのBODYで渡ってくるデータは以下。 { \"annotation\": { \"query\": \"hogehoge\", \"name\": \"hoge\", \"datasource\": \"JSON-ikuty\", \"enable\": true, \"iconColor\": \"rgba(255, 96, 96, 1)\" }, \"range\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\", \"raw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" } }, \"rangeRaw\": { \"from\": \"2015-12-22T03:16:16.275Z\", \"to\": \"2015-12-22T03:16:18.102Z\" }, \"variables\": { \"variable1\": { \"text\": [ \"All\" ], \"value\": [ \"upper_25\", \"upper_50\", \"upper_75\", \"upper_90\", \"upper_105\" ] } } } これに対して以下の応答を返すとイメージのようになる。 以下はAnnotationEvent型と型が一致している。 isRegionがtrueの場合、timeEndを付与することでアノテーションが領域になる。 isRegionがfalseの場合、timeEndは不要。 tagsにタグ一覧を付与できる。 [ { \"text\": \"text shown in body\", \"title\": \"Annotation Title\", \"isRegion\": true, \"time\": \"1450754170000\", \"timeEnd\": \"1450754180000\", \"tags\": [ \"tag1\" ] } ]
PostgreSQL スキーマをコピーする
スキーマをコピーする方法はない。 代わりに以下の方法で同じ効果を得る。 スキーマ名Aをスキーマ名Bに変更する スキーマ名Bの状態でpg_dumpする スキーマ名Bをスキーマ名Aに変更する スキーマ名Bを作成する pg_dumpしたファイルをリストアする Statementは以下の通り。 $ psql -U user -d dbname -c \'ALTER SCHEMA old_schema RENAME TO new_schema\' $ pg_dump -U user -n new_schema -f new_schema.sql dbname $ psql -U user -d dbname -c \'ALTER SCHEMA new_schema RENAME TO old_schema\' $ psql -U user -d dbname -c \'CREATE SCHEMA new_schema\' $ psql -U user -q -d dbname -f new_schema.sql $ rm new_schema.sql [arst_adsense slotnumber=\"1\"]
Redshift テーブル設計のベストプラクティス
どのようにテーブル設計するとパフォーマンスを得られるか. 公式がベストプラクティスを用意している. Redshiftのベストプラクティスが先にあってER図が後なのか、 ER図に対してベストプラクティスを適用するのか、 実際は行ったり来たりするようなイメージ. ER図とは別に何を考慮すべきなのか読み進めていく. [arst_toc tag=\"h4\"] ソートキー テーブル作成時に1つ以上の列をソートキーとして設定できる. 設定するとソートキーに準じたソート順でディスクに格納される. ソートキーに関するベストプラクティスは以下の通り. 最新のデータを得たい場合はタイムスタンプ列をソートキーにする. 1つの列に対してwhere句による範囲指定or等価指定をおこなう場合はその列をソートキーにする. ディメンションテーブルを頻繁に結合する場合は結合キーをソートキーにする. ファクトテーブルを中心にディメンションテーブルが4つある構造があるとする. ファクトテーブルにはディメンションテーブルのPKが入り関連している. また、ファクトテーブルに日付カラムがあり、常に最新のレコードが欲しいとする. ベストプラクティスによると、 各テーブルの各カラムに以下のようにソートキーを設定する. 分散スタイル クエリの実行を複数のクラスタ(コンピューティングノード、スライス)で実行するために、 それらに1)データを配信して 2)計算させて 3)結合、集計する というステップが必要になる。 最後のステップ3を達成するために、データの再び配ることが必要となる。 全体として最適となるように、1),2),3)の効率を高める必要があるが、 あらゆるデータ、条件について同じ戦略で最高の効率を得ることはできず、 設計者が戦略を指定するパラメタとなっている。 この戦略を分散スタイルと呼んでいる. 分散スタイルとして以下の3通りが用意されている. 各々だけ読むとさっぱり意味がわからないが、結局のところ再分散のコストをいかに減らすか、 というところに着目すると合点がいく. EVEN 分散 特定の列に含まれている値と関係なくラウンドロビンで複数のスライス間で行を分散させる. テーブルが結合に関与していない場合や、キー分散、ALL分散のどちらが良いかわからない場合に指定する. キー分散 キー分散のキーとは結合キーのこと. 特定の列に含まれている値に従って複数のスライスに行を分散させる.キーが同じということは「同じデータ」であり「同じデータ」達を同じスライスに分散させる意味がある. 共通の列からの一致する値が同じスライスにまとめられるよう努力する. ALL 分散 テーブル全体のコピーが全てのノードに分散される. EVEN分散、キー分散によってテーブルの一部が各ノードに配置されているときにALL分散を行うことでテーブルが関与しているあらゆる結合で全ての行が確実にコロケーションされる. 何が嬉しいのかわかりづらいが、EVEN分散やキー分散では、クエリ実行に伴って、再び必要なデータをコピーする(再分散する)必要が発生する可能性が生じる.ALL分散であればその可能性がなくなる. AUTO 分散 (デフォルト) テーブルデータのサイズに基づいて最適な分散スタイルを割り当てる. まず小さなテーブルにALL分散を設定し,テーブルが大きくなるとEVEN分散に切り替える. 分散スタイルを明示的に設定しないとAUTO分散になる. まず、ファクトテーブル関連する1つのテーブルの共通の列に基づいて分散させる. 関連するテーブルの選び方の観点は大きさで最もレコード数が大きいテーブルを選択する. 以下の構造では、ファクトテーブルとディメンションテーブル1が dim1_keyというキーを使って結合している. そこで, ファクトテーブルのdim1_key、ディメンションテーブル1のdim1_keyを分散キーとして採用する.(緑) ここまでで、dim1_keyの値が一致するレコードが同じスライスにコロケーションされる. キー分散に使うキーは1組のみ. 残りのテーブルについてはEVEN分散かALL分散を用いる. 選び方は上記の通り. テーブルのサイズが小さいのであれば、ALL分散により再分配の可能性がなくなり選びやすい. 圧縮エンコーディング 通常のRDBのように行方向の固まりを記録する場合、各列の値は型や値の傾向がまちまちであるため、 一様に圧縮しようとしても高い圧縮率を得られない. 対して、列方向の固まりを記録する場合、各列の型は同じだし値の傾向が似ていることが多いため、 高い圧縮率を得られる可能性がある. ただし、値の傾向により圧縮アルゴリズムを選択する必要がある. 公式で挙げられているアルゴリズム. 結局試してみないとわからない、というのはある. (Zstandard強すぎないか?) raw エンコード 圧縮をおこなわない. ソートキーが設定されているときはrawエンコードが設定される BOOLEAN、REAL,DOUBLE PRECISION型もraw. AZ64 エンコード Amazon 独自の圧縮エンコードアルゴリズム より小さなデータ値のグループを圧縮し、並列処理に SIMD (Single Instruction Multiple Data) 命令を使用する 数値、日付、および時刻データ型のストレージを大幅に節約する バイトディクショナリエンコード バイトディクショナリエンコード ディスク上の列値のブロックごとに、一意の値の個別のディクショナリを作成する 列に含まれる一意の値の数が制限されている場合に非常に効果的 列のデータドメインが一意の値 256 個未満である場合に最適 CHAR 列に長い文字列が含まれる場合に特に空間効率が高まる VARCHAR 列に対しては、LZO などの BYTEDICT 以外のエンコードを使用する デルタエンコード 列内の連続する値間の差を記録することにより、データを圧縮 日時列にとって非常に有用 差が小さいときに特に有用 LZO エンコード 非常に長い文字列を格納する CHAR および VARCHAR 列、特に製品説明、ユーザーコメント、JSON 文字列などの自由形式テキストに適している Mostly エンコード 列のデータ型が、格納された大部分の値で必要なサイズより大きい場合に有用. たとえば、INT2 列などの 16 ビット列を 8 ビットストレージに圧縮できる ランレングスエンコード 連続して繰り返される値を、値と連続発生数 (実行の長さ) から成るトークンに置き換える データ値が連続して繰り返されることが多いテーブルに最適 たとえば、テーブルがこれらの値でソートされている場合など Text255 および Text32k エンコード 同じ単語が頻繁に出現する VARCHAR 列を圧縮する場合に有用 Zstandard エンコード 多様なデータセット間で非常にパフォーマンスのいい高圧縮比率を提供 製品説明、ユーザーのコメント、ログ、JSON 文字列など、長さがさまざまな文字列を保存する CHAR および VARCHAR 列に対して有用 圧縮エンコーディングをテストするためには、 各アルゴリズムで差が出るように大量のデータを用意する必要がある. 公式には、テストするために大量のデータを用意することは難しいので デカルト積ででっち上げる手法が案内されている. 例えば、こんな感じにデータをでっちあげる. create table cartesian_venue( venueid smallint not null distkey sortkey, venuename varchar(100), venuecity varchar(30), venuestate char(2), venueseats integer ); insert into cartesian_venue select venueid, venuename, venuecity, venuestate, venueseats from venue, listing; このうち、venunameに対して各エンコーディングアルゴリズムを適用して格納するデータを作る. create table encodingvenue ( venueraw varchar(100) encode raw, venuebytedict varchar(100) encode bytedict, venuelzo varchar(100) encode lzo, venuerunlength varchar(100) encode runlength, venuetext255 varchar(100) encode text255, venuetext32k varchar(100) encode text32k, venuezstd varchar(100) encode zstd); insert into encodingvenue select venuename as venueraw, venuename as venuebytedict, venuename as venuelzo, venuename as venuerunlength, venuename as venuetext32k, venuename as venuetext255, venuename as venuezstd from cartesian_venue; 知りたいことは、encodingvenueの各列で実際に使われているディスク容量. 以下のようにして各列で使用される1 MBのディスク ブロック数を比較するらしい. rawが203に対してBYTEDICTが10. つまりBYTEDICTにより20:1の圧縮率を得られた例. select col, max(blocknum) from stv_blocklist b, stv_tbl_perm p where (b.tbl=p.id) and name =\'encodingvenue\' and col < 7 group by name, col order by col; col | max -----+----- 0 | 203 1 | 10 2 | 22 3 | 204 4 | 56 5 | 72 6 | 20 (7 rows) まとめ 公式のベストプラクティスを追ってみた. 面倒だけれども結構力技で出来ているなという印象. 与えられたスタースキーマからある程度決まったやり方でパラメタを選択できそう. 実データでやったら迷うことは必至w ソートキーと分散スタイルの選択は分散コストに影響する. 圧縮エンコーディングの選択はディスクストレージに影響する. 理解したら実際に試行錯誤していくしかないイメージ.
Amazon Redshift概要 パフォーマンス総論
概要 各論に入る前に総論。 MPPでクエリ実行するために必要な制限事項を設計/実装するために、 とりあえず、何故その制限事項が必要なのかを理解しておく必要がありそう。 [arst_toc tag=\"h4\"] 並列処理 MPP(Massively Prallel Processing). シンプルで安価なプロセッサを多数集積して一台のコンピュータとする手法. 各ノードの各コアは、同じコンパイル済みクエリセグメントをデータ全体の一部分に対して実行する。 テーブルの行をコンピューティングノードに分配し分散処理する。 列指向 通常のアプリケーションとデータウェアハウスではクエリで取得したいデータが異なる. 通常のアプリケーションは行の大方の列が欲しい一方で、データウェアハウスは行の中の一部の列が欲しい。 データウェアハウスが1行の全ての列を取得する方式を使用すると、ほとんどの列は無駄になってしまう. ある行の1列にだけ関心がある状況で15行分欲しい場合、行指向であれば100回、列指向であれば5回のディスクI/O。 データ圧縮 列指向で同じ列のデータを取る場合、同じ列に入るデータの乱れ方には傾向があるはずなので、データ圧縮が効きやすい。 データ圧縮によりディスクI/Oがさらに減少する。 クエリオプティマイザ MPP対応のクエリオプティマイザ。複数のコンピューティングノードで並列処理するための最適化が走る。 結果のキャッシュ リーダーノートでキャッシュする必要性があるクエリと結果をキャッシュする。 サイズの大きなクエリ結果セットはキャッシュしない。 キャッシュするか否かは、キャッシュ内のエントリ数とAmazon Redshiftクラスターのインスタンスタイプが含まれる。
