About connecting the dots.

data science related trivial things

Amazon Athena の Query Result Reuse で同じクエリの結果を高速に取得する

この記事は,AWS Analytics Advent Calendar 2022 の 5 日目の記事になります.

qiita.com

11/8 に Amazon Athena が Query Result Reuse をサポートしました.この機能,要するにクエリ結果のキャッシュが使えるようになったとのことで,似たようなクエリを何度も試すような場合には,結果を高速に取得できるようになります.この記事では,実際にいくつかのパターンで利用して,どのくらい効果が出るかを確認できればと思います.

aws.amazon.com

試してみる

集計データ

まずはマネコン上のクエリエディタから,実際にクエリを投げてみたいと思います.ちなみにこの機能は Athena のバージョン 3 エンジンでのみ使用可能なため,古いバージョンのエンジンを使っている方は,マネコンの左側メニュー Workgroups から,エンジンバージョンを最新の 3 に変更しておきましょう.今回使用したのは手元のテストデータで,5GB ほどのものになります.このくらいだとあまりキャッシュの恩恵はないというのはおいておくとして...

select
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
    , count(1)
    , sum(amount_sold)
from
    sales
    , customers
    , times
where
    sales.cust_id = customers.cust_id
    and sales.time_id = times.time_id
group by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
order by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
;

クエリを実行した結果は次のとおりです.データスキャン量が 5GB で,実行時間が 15 秒程度となっています.このデータは GZip 圧縮のものだったので,多少オーバーヘッドが出ており時間がかかっています.Parquet フォーマットならもっと早いかとは思います.

さて,ここで画面に Reuse query results というメニューがあるのが確認できるかと思います.こちらがすでにオンになっているので,同じクエリをもう一度実行すると,今度はキャッシュが使用されるはずです.結果は以下の通り,"Completed - using resused query results" と表示されており,実際にスキャンされたデータ量が "-" となっていることも確認できます.結果は 274ms ということで,結果を取ってくるぶんの時間だけですね.


生データそのまま

それでは次に,大きなデータを吐き出してみます.Athena は大規模データをそのまま持ってくるためのものではないので,結構時間がかかります.正確には,CTAS 等でテーブルとして書き出すのであれば高速に動作しますが,クエリ結果として取得する場合は 1 つの csv にまとめる形になるため,並列処理が働かず,結果としてファイルの書き出し部分がボトルネックとなって重くなってしまいます.これは大規模分散データ処理の宿命なので,仕方ないところではありますが.

select
    *
from
    sales
;

得られた結果は以下のとおりです.スキャン量は 4 GB 弱ですが,実行に 15 分以上かかりました.となりのタブにある統計情報を見てみると,全部でレコード数が 1.5 億弱行で,圧縮されてない元データサイズは 16.92GB だったことがわかります.

それでは,これをキャッシュを使ってもう一度実行してみるとどうなるでしょうか.当然ですが 229ms で結果が完了します.すばらしいですね.ただ今回はクエリエディタ上からの実行で,先頭の一部の行の結果しか取ってきてないため高速ですが,例えば CLI で結果を丸ごと取ってくる場合には,10GB を超えるデータをもってくるぶんの時間はかかると思われます.


キャッシュ時間の設定

キャッシュ再利用時間の設定は,コンソールの Reuse query results のところから設定でき,1 分から 7 日間までの間で選択することができます.このあたりは,クエリ対象データの性質に応じて適宜変えると良いでしょう.このキャッシュ再利用時間設定は,クエリ実行を行う毎に別の値を用いることができます.例えば boto3 経由でクエリ実行する場合には,下記の ResultReuseByAgeConfiguration を ResultReuseConfiguration オプションで設定することが可能です.

import boto3

client = boto3.client('athena')
response = client.start_query_execution(
    WorkGroup='my_work_group',
    QueryString='SELECT * FROM my_table LIMIT 10',
    ResultReuseConfiguration={
        'ResultReuseByAgeConfiguration': {
   	    	'Enabled': True,
     		'MaxAgeInMinutes': 60
        }
    }
)

AWS CLI でも同様に指定可能です.

aws athena start-query-execution \
  --work-group "my_work_group" \
  --query-string "SELECT * FROM my_table LIMIT 10" \
  --result-reuse-configuration \
    "ResultReuseByAgeConfiguration={Enabled=true,MaxAgeInMinutes=60}"

キャッシュされたデータとその挙動

置かれている場所

さて,このキャッシュされたデータはどこに置かれているのでしょうか.Athena のドキュメントには OutputLocation が条件として書かれているので,普通に考えれば S3 に置かれた結果ファイルを再利用していると想像されます.では,実際にそちらを確認してみましょう.下記のように,Unsaved/YYYY/MM/DD というパスでクエリの実行結果が保存されています.

この結果が実際に使われているかを確かめるために,データを他の場所に移動してみましょう.まずはデータ本体から.

この状態でクエリを再度実行すると,キャッシュが使われることなく,スキャンが走ります.ということで,少なくとも現状では,やはりクエリ結果の出力フォルダにあるデータがキャッシュとして使われていると考えて良さそうです.もちろん実ワークロードで使う際には,ここのファイルを安易に消せるような権限設定にするべきではないわけですが.

ちなみにメタデータ側を削除すると,"using reused query results" は表示されるのに,結果は取得できないという状況になります.


発動条件について

キャッシュ再利用を発動させるためには,ドキュメントに書かれている内容が必要とのことです.特に気になるのは `The query string is an exact match.` とのことで,同一クエリでも半角スペースが一つ入ったり,コメントが入っていたらどうなるのかが気になります.ということで,こちらも検証してみました.

以下のように,2 行目に半角スペースを一つ追加してみたところ,書かれている通りキャッシュは使われず再実行が走りました.

select
     calendar_year
    , calendar_month_number
    , country_id
    , cust_city
    , count(1)
    , sum(amount_sold)
from
    sales
    , customers
    , times
where
    sales.cust_id = customers.cust_id
    and sales.time_id = times.time_id
group by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
order by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
;

同様に,コメントを追加したら,やはり結果は再利用されませんでした.

/* Test comment */
select
     calendar_year
    , calendar_month_number
    , country_id
    , cust_city
    , count(1)
    , sum(amount_sold)
from
    sales
    , customers
    , times
where
    sales.cust_id = customers.cust_id
    and sales.time_id = times.time_id
group by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
order by
    calendar_year
    , calendar_month_number
    , country_id
    , cust_city
;

おそらくは,内部的にクエリ文字列をハッシュ変換しており,それと一致しているものがあるかどうかで同一性の確認をしているのだと推測できます.便利ではありますが,気をつけて使わないといけない部分ですね.

これ起因で,QuickSight から接続した場合も,また AWS SDK for Pandas から接続した場合も,現状だとキャッシュ再利用は働かないようです.QuickSight からのクエリに関しては,以下のようにハッシュが入った形の名前がついていたり,また冒頭にコメントが入ってしまう仕様のようです.

/* QuickSight d9425c29-18c5-4616-9043-973213fbdf0d */ SELECT COUNT(*) AS "count", SUM("amount_sold") AS "9ab75332-3ccf-484d-ad58-9a010e03fd49.amount_sold_sum" FROM "AwsDataCatalog"."sh10_gz"."sales"

AWS SDK for Pandas の場合は,こちらもハッシュ値のついた temp_table にデータを保存する形になるようなので,キャッシュは使えないということになります.

CREATE TABLE "sh10_gz"."temp_table_7e183e2045e34e1fa52fd0fa8b12e29c" WITH( external_location = 's3://aws-athena-query-results-666254511816-us-east-1/temp_table_7e183e2045e34e1fa52fd0fa8b12e29c', format = 'PARQUET') AS select * from sales limit 1000000

まとめ

本記事では,Amazon Athena の Query Result Reuse 機能について,少し突っ込んで中身を色々と確認してみました.いくつか制約はあるものの,色々な可能性のある機能だと思いますので,ぜひ活用していきましょう! また,AWS Analytics Advent Calendar 2022 の他の記事もぜひお楽しみに!

echo dot から Fire TV で Radiko を再生する方法

少し変化球ですが,広義の技術ネタと考えてここに記します.合計 4 時間くらい四苦八苦してたので,レアだと思いますがもし参考になる人がいればということで.内容はタイトルの通りですが,図に示すと以下の通りです.

要するに,echo dot にボイスコマンドを投げて,Fire TV の Radiko を操作して,サウンドバーから音を出したいということです.

実際に達成した方法としては,ターゲットデバイスを Fire TV として (1) Fire TV をオンにする,(2) 「アレクサ、ラジコでnhk fm」をまとめた Alexa の定型アクションを作成し,これを普段よく聞くラジオ放送局ぶんだけ作成する,です.

背景

こんな面倒なことをせずに,直接 echo dot で Radiko の音を出せばいいのではと思うかもしれませんが,echo dot の音がしょぼいので,もう少し良い音が出るサウンドバーから鳴らしたかったというのが理由です.その場合,echo dot とサウンドバーを Bluetooth で直接繋ぐほうが直接的なやり方なんですが,自分の環境だとうまくいきませんでした.理由は (1) サウンドバーの音声インプットは,基本 HDMI でつないだテレビが優先される形になっており,入力を Bluetooth に切り替えるためには,明示的にサウンドバーのリモコン操作が必要になります.(2) かつ HDMIBluetooth でベース音量が大きく違っており,音量変更のリモコン操作も併せて必要なのもストレスでした.(3) さらにこれが一番致命的なのですが,Bluetoothサウンドバーに繋いでいると,普段はテレビからの HDMI を優先しているので,echo dot からの音声出力が基本聞こえないという点です.そのため通知の確認やタイマーのセットとかが一切使えなくなります.これらの理由から,直接接続は断念せざるを得ませんでした.

そこで考えられるのが,(1) echo dot 自体を買い替えて,そもそも良い音質で聴けるようにする,もしくは (2) Fire TV を購入して,そちらをサウンドバーにつないで Radiko の音を鳴らす,です.前者は echo studio が思い浮かびますが.単品で 3 万近くしますし,この時点ですでにサウンドバーを持っていた自分にとっては,魅力的な選択肢にはなりませんでした.そこで後者を選択しました.これなら Fire TV Stick で 3000 円ほどで済みますし,すぐに設定できるとたかを括っていました.しかし,これが思いのほか難儀でした.

Radiko 再生が意外と難しい

Fire TV で Radiko を再生するには,大きく分けて 3 つのやり方があります.(1) Alexa の Radiko スキルを使う,(2) Fire TV にある Radiko アプリを使う,(3) Fire TV 上の Silk Browser などで Radiko のデスクトップ版ウェブサイトを開いて再生する,です.

(1) について,Alexa スキルでは,基本的にデバイス単位での機能の有効化/無効化はできません.そのため echo dot に対して Radiko の再生コマンドを投げると,基本的に echo dot 上で再生してしまいます.そこで明示的に「アレクサ、Fire tvでradikonhk fmを再生」といったコマンドをいろいろな言い方で投げてみましたが,うまく認識してもらえませんでした.結局うまく機能させるためには,Fire TV のリモコンで「アレクサ、radikonhk fmを再生」のようなコマンドを打つ必要があり,当初の目的からは遠ざかってしまいした.

(2) については,Fire TV の Radiko アプリがくせもので,自分には正直うまい使い方がよくわかりませんでした.そもそも立ち上げてもそのままでは再生されず,別途ボイスコマンドが必要となる仕様のようで,そのボイスコマンドも Fire TV のリモコンから実行する必要があるみたいです.echo dot から「アレクサ、Fire tvでradikoつけて」と実行すればアプリ自体は立ち上げてくれるものの,そこでラジオの再生はされないというなんとも意味のない状態になってしまいます.

(3) について,こちらは特に問題なく動作しタイムシフト再生まで可能なのですが,そもそもボイスコマンドだけで操作できず,リモコンでのカーソル移動操作が求められるため,手間が大きすぎて断念しました.

ということで,正攻法ではにっちもさっちもいかない状況に陥ってしまいました.echo dot と Fire TV のリンクだったり,グループやスピーカーグループなど試してみたものの,特に状況は改善しませんでした.Youtube とか Prime Video だと,echo dot から再生できないこともあってか,ボイスコマンドで echo dot から Fire TV 操作は問題なくできたんですけどね...

定型アクションというソリューション

上記の 3 つの案だと,(2) と (3) はどう転んでもボイスコマンド一発では実現できない仕様のように思われるため,(1) を軸に考えることになります.ここで (1) の課題を思い返してみると,Radiko の再生コマンドを実行するデバイスを強制的に指定できさえすれば問題が解決することがわかります.そこで出てくるのが定型アクションです.以下に具体的な設定例を示すのですが,一番下のところに「デバイス」とあるように,コマンドを実行する対象のデバイスを指定することができます.ということで,最終的にこれで問題解決となりました.ちなみに Firetarou というのが Fire TV のデバイス名です*1.一点だけ問題があるとすると,一つのコマンドでは一つの放送局の再生しかできないので,よく聞く放送局分だけ登録しておく必要があることですが,まぁこれは最初ちょっと手間がかかるだけなので,特に問題はないと判断しました.


おまけ

ということで,ぶじやりたかったことが実現できたんですが,最後にもうひとつ欲をいえば達成したかったのが,画面をオフにした状態での Radiko の再生です.Fire TV は基本的に TV と連動するデバイスなので,上記の定型アクションを実行すると必然的にテレビの画面がついてしまいます.まぁこの構成を取ろうと思った時点で
これに関しては許容するつもりだったのですが,可能なら上記のオプションも持ちたいと考えました.こちらもいくつか検討した結果,テレビ側の HDMI 連動設定で,テレビ → 連動機器電源オフを「連動しない」設定にすることで,定型アクション実行後にテレビリモコンからテレビ電源を切っても,Radiko がそのまま再生し続ける状態を作り出すことができました*2.まぁこれは結局リモコン操作が入るので,あまりスマートではないですが,長時間 Radiko を聞きたいときにはまぁやってもいいかなくらいのオプションです*3

あと少し離れた別の話として,個人的には操作リモコンの数を可能な限り増やしたくなかったので,上記のソリューションによって Fire TV / サウンドバーのリモコンを基本的に使わずに済み,TV リモコンに統一できるようになったのが朗報でした.

そんなわけで,たいしたことない Alexa まわりの試行錯誤でしたが,似たような記事が Web 上に見当たらなかったので,どこかの誰かに参考になることもあるかもしれないと思って書いておくことにしました.

*1:この名前のセンスについては触れないでください.

*2:自分の使っている REGZA には画面表示オフ機能がないので,この手を取るしかなかったんですが,SHARPSONY のテレビだと,画面表示オフ機能があるみたいなので,そちらの活用もできそうです.

*3:本当は定型アクションに REGZA の電源オフも入れたかったのですが,定型アクションはカスタムコマンドが最後に来なければいけないという制約があるようで,含めることができませんでした...

Amazon QuickSight の 1-click 埋め込みダッシュボード機能を試してみた

この記事は AWS Analytics Advent Calendar 2021 の 3 日目の記事になります.というか気がついたら 2 年半もの間,一本も記事を書かないままに時間が経ってしまっていたんですね.その間に世の中はコロナになって世界中大混乱だったりしているわけですし,個人的にもいろんなことがありました*1.とはいえ相変わらずデータ界隈の片隅でボチボチとやっております.

qiita.com

ダッシュボードの埋め込み

さて,ここからが本題です.今回は珍しく BI の話*2.以前お仕事で Tableau Server/Desktop を触ってたことがあるんですが,使い勝手は良いものの,ライセンスの問題でなかなか社内に大規模展開するのが大変だったり,サーバ運用,特に定期的なソフトウェアのバージョンアップデートが結構大変だったことをよく覚えています*3.気がついたら今はクラウド全盛で,BI ツールも SaaS とかサーバーレスのも増えてきており,この辺りがだいぶ楽になってきてるなぁと思う次第です.今回取り上げる Amazon QuickSight もそんなサーバーレス型の BI サービスですね.BI って結構社内ポータルに埋め込んだり,BI 以外の他のデータとまとめて一箇所に表示したりとかって使い方すると思うんですが,その場合いわゆる埋め込み機能が必要になります.社内の各部署にお知らせをして,別のダッシュボードツールの使い方の説明をして,それをみてもらうというのは結構敷居が高かったりします.そんな場合には,既に使われているところにダッシュボードも集約したい,っていうのが結構よくある話ですよね.まぁあとは SaaS とかやっている会社で,ユーザー向けにダッシュボード提供とかやる場合もあるかもしれないです*4

さて QuickSight にも以前から埋め込み機能があったんですが,API オペレーション前提で,コードをガッツリ書いて埋め込む元のページにデプロイしないといけなかったんですよね.埋め込みハンズオンが公開されているので,みていただければ分かりますが,コマンドラインでの作業が結構多くて,実際に試すまでの手順が長かったんですよね.

awsj-assets-qs.s3.ap-northeast-1.amazonaws.com

このページの一番下のフローチャートとかみていただくとわかるんですが,アプリでダッシュボードを埋め込んだページを表示する際に,QS のユーザー認証や IAM ロールの権限取得,埋め込みダッシュボードの URL 取得,ってプロセスをやっているので,パッと実装するのは敷居が高いです.

1-click 埋め込み機能

ここで最近出てきた 1-click 埋め込み機能*5なんですが,響きからして簡単にできそうな感じであふれてます.これみる限り,ほぼ GUI 作業だけで手軽にページ内埋め込みができそうなんで,実際に試してみましたよというのが今回の話.

aws.amazon.com

ということで,実際に試してみた内容を以下にまとめました.

ダッシュボードを共有して埋め込みコードを取得

データセットを読み込んでダッシュボードを作成するというのは,AWS のブログにまとまってるベーシックハンズオンとかやればわかるので,ここでは割愛.

aws.amazon.com

ダッシュボードを作成したら,画面右上からダッシュボードの共有ボタンを押して,共有したい人の権限を追加したら OK です.別に自分しかみないんだったら,特に誰も追加しなくてもいいんですけどね.

f:id:SAM:20211130142455p:plain

上記の共有設定のページの上側に,埋め込み用コードを取得する「埋め込みコードをコピー」リンクがあるので,そちらを押すと iframe のコードがクリップボードにコピーされます.内容はこんな感じ.以下の XXX... の部分は,12 桁の AWS アカウント ID と,YYY... のところは「7360487b-a372-4b15-bc5e-c327ac079825」みたいなダッシュボード ID(要はダッシュボードの URL の末尾のハッシュ文字列です)に置き換えてください.

<iframe
    width="960"
    height="720"
    src="https://ap-northeast-1.quicksight.aws.amazon.com/sn/embed/share/accounts/XXXXXXXXXXXX/dashboards/YYYYYYYYYYYYYYYY">
</iframe>

許可ドメインを設定して実際に埋め込み

埋め込みを実際にする前に,あともう一手間だけ必要で,許可ドメインの設定を行います.これ,実際に埋め込んだページのコードをみたら iframe の内容がコピペできてしまうので,明示的に埋め込みをアクセスしたドメイン以外からだとアクセスできないようになっています.そしてデフォルトではドメインが登録されていない状態です.そのためこのブログのドメインを,許可ドメインに追加します.

まぁこれも最近実装されたアクセス元 IP アドレス制限機能とか使えば,コードをコピペして会社の外に持ち出すとかしても,会社のネットワークの IP アドレス以外からアクセスしても表示されないような制限をかけられますし,また埋め込んだダッシュボードを開く際に認証のポップアップが開くので,SSO とかで社内ネットワークからしかつなげないような IdP を設定しておけば,同様の制限はやはりできます.まぁ何重にも防御できるようにしましょうという話ですね.このあたりよくできてます.

aws.amazon.com

さて,実際にドメインを登録するには,画面右上から「QuickSight の管理」を選択して...

ドメインを追加します.ここでは,このブログのドメインを追加しました.ここではやってませんが,ポート番号の指定とかまでできるみたいですね.

f:id:SAM:20211130144932p:plain

以上です.

実際に埋め込みを表示してみる.

これで埋め込みの準備が完了したので,実際に画面を表示してみます.はてなブログの場合は,ブログの編集画面に直接 iframe タグを貼るだけなので非常に簡単ですね.とはいえ自分のアカウントの内容をここにまるっと晒すのもアレなので,画像だけでご勘弁ください...いちおう適用イメージは以下の通りです.ログインしていない状態でページを開くと,以下のようにエラーが出て,ログインを促されます.

f:id:SAM:20211130163754p:plain

上記画像の「sign in again」リンクをクリックすると,ログイン画面がポップアップで開くので,まずはアカウント名を入れて,QS のユーザー名と ID を入力してログインすれば,以下のようにちゃんと埋め込まれたダッシュボードが表示されます.今回は面倒なので ID/Pass でやっちゃいましたけど,SSO を設定しておけばもっとスムーズにログインできるはず...

f:id:SAM:20211130164105p:plain

カレンダー選択ビューを表示できており,きちんとインタラクティブに動いていることがわかるかと思います.このダッシュボードは適当に作ったやつですけど,サンプルの売り上げデータを時系列で並べて,QS の機能で提供されている時系列予測を適用しています.

f:id:SAM:20211130145810p:plain

ちなみに先ほどの許可ドメインを消してからもう一度アクセスすると,以下のように "ap-northeast-1.quicksight.aws.amazon.com refused to connect" となっており,アクセスできないことが確認できるかと思います.

f:id:SAM:20211130150325p:plain

既存の API ベースの埋め込み機能との比較

最後に,既存の埋め込み機能との比較をしておしまいにしたいと思います.ざっくりまとめると以下の通りです.

1-clock 埋め込み
  • とてもお手軽,10 分で埋め込みできる
  • ダッシュボードの埋め込みにのみ対応,管理・分析系機能の埋め込みは今のところ対応していない
  • 未ログインでアクセスした場合,ポップアップ経由で QS へのログインが必要
従来の埋め込み
  • 1-click と比べると覚えることが多い.API 周りの挙動とかコード記述も必要
  • ダッシュボードを埋め込む以外に,管理・分析系機能部分の埋め込みにも対応している
  • ログイン処理をコード側で行うため,ユーザー側でのログイン処理は発生しない

現状だと 1-click の埋め込みは,既存の埋め込み機能を完全に置き換える形にはならないようなので,適材適所で使い分けする形になるでしょうか.ただ社内ポータルとかでのダッシュボード共有のケースであれば,ユーザーは社内 IdP の認証を済ませている状態でしょうし,1-click 埋め込みを使ったほうがはるかに楽に済むケースが多いように思います.

*1:異動したりロールが変わったり喘息になったり家を買ったり...それにしても最近の住宅相場の値上がりっぷりは本当にやばいですね.

*2:BI とはなんぞやって人は,こちらのページによくまとまっているので,まずはご一読ください.

*3:まぁ Tableau Online とかの選択肢もありますが,それはそれで料金が高いんですよね...

*4:自分はそのあたりを実務で扱った経験がないのでよくわかりません...

*5:以前からのも,この 1-clock のも両方,埋め込み機能は QuickSight の Enterprise Edition でのみ利用できる機能なので,その点ご注意ください.ただ Enterprise Edition だと,$5 キャップ付き従量課金の Reader ライセンスを利用できるので,組織内に大きく展開できるので普通に便利.

SageMaker built-in algorithm の XGBoost で変数重要度を確認する

AWS機械学習サービス SageMaker にはビルトインアルゴリズムがいくつも用意されていて,その中には定番の XGBoost も含まれています.XGBoost の定番のユースケースは,モデルを作って変数重要度を確認して,という流れだと思うんですが,ビルトインアルゴリズムだとそのあたりの関数が提供されていません.ホスティングはできるけど,逆にいうとホスティングしかできないわけです.

作成済みモデルの変数重要度がみれないか確認したところ,なんのことはない,S3 に出力されるモデルファイルの tar.gz を回答して,pickle でロードしてあげれば普通にいけました.具体的なコードは以下のような感じです.

aws s3 cp s3://BUCKET_NAME/MODEL_PATH/model.tar.gz
tar zxvf model.tar.gz
import xgboost as xgb
import pickle as pkl
import matplotlib.pyplot as plt
 
plt.style.use('ggplot')
 
model = pkl.load(open('xgboost-model','rb'))
xgb.plot_importance(model)

ちょっとした小ネタでした.

書評: 人工知能システムのプロジェクトがわかる本

もうすっかり四半期に一回しかブログを書かないような感じになってますが,変わらずデータ分析業界の片隅で細々と生きております.さて,今回は著者の方より献本をいただいたので,書評を書きたいと思います*1

この手の機械学習*2そのものではなく,そのプロセスやシステムに焦点を当てた本はこれまでもありました.従来書だと,機械学習システムを内製するエンジニア向け,機械学習システムを発注する側向けがありますね.そしてついに,機械学習システムを受託で開発する側の本が出てきたので,これで足りないピースがそろった感があります (w いずれの本も機械学習という事象を正しく捉えていると思うんですが,視点を変えるとこうも内容が変わるもんだなぁと興味深く思います.

仕事ではじめる機械学習

仕事ではじめる機械学習

人工知能システムを外注する前に読む本~ディープラーニングビジネスのすべて~

人工知能システムを外注する前に読む本~ディープラーニングビジネスのすべて~

この本を読んで得られるもの

いわゆる SIer の人が,RFC・要件定義・PoC・開発・および保守運用といった通常のシステム開発フレームワークにのっとった形で,機械学習システムの作り方(や既存のシステムとの違い)を理解することができる

この本のスタンスは非常に明確で,SIer に勤務しており,これから機械学習システムの提案・受託を行おうとしている(もしくはすでに行なっている)人です.本編の流れはほぼひとつのケーススタディに沿っており,その中で実際に考慮すべき点,通常のシステムとの違いについてわかりやすく説明がなされています.機械学習システムは他のシステムと比べて,要件定義の難しさや手戻りの可能性,また保守運用の中での継続的なモデル改善の必要性といった点で,多くの不確実性を孕んでいます.そのため,こうした点に特別の注意を払って RFP は作られる必要があり,またそれに応じた提案を行う必要があります.私の個人的な経験でも,このあたりの難しさをわかりやすく説明しているものはなく,その点非常に多くの人に有益なように思います.

個人的にためになった点

私自身はいわゆる SIer に勤めた経験はなく,このようにカッチリしたシステム受託のプロセス周りの知見がそれほどありません.なので,SIer 的なプロセスを,機械学習システムという自分に馴染みのあるものを通して体感的に理解できたことはなかなか良いものでした.

また,個人的にクリーンヒットだったのは,「人工知能に期待しすぎる人に対する返し方」というコラムです.ちょっと長いですが引用すると,

人口知能に過度な期待を抱いている人と一緒にシステムの目的を協議する際には,彼らの意識を変えなければなりません...(中略)...利用者 「これくらいの精度はうちの○○さんでもできそうだ」→「入社直後の人が○○さんくらいの業務ができるようになるのはすばらしいことですよね...(中略)...利用者「検知したい不正の70%しか検知できてないじゃないか」→「人が検知したときには,これ以上の漏れがあると聞いていますので,人によるチェックも併せてやると,人の手間も減らしながら精度が上がりそうですよね」

私もいままで機械学習に詳しくない人とお話をする中で,どのように相手側の期待値を正しいところに落ち着かせ,目線を揃えていくかというところに結構神経を使うことがあります*3.こうした点も含めて,実体験からきている(であろう)こうした記述が,非常に参考になります.ネット上に溢れている言説の大半は,機械学習のエキスパート(大体の場合研究者か内製企業のエンジニアやデータサーエンティスト)か,機械学習ブームを煽るメディアサイ*4の言説で,このように地に足のついた言説をみる機会はほぼないように思います.

あと,業務システムに組み込むときの運用含めた注意点として,機械学習に詳しくない担当者が予測値をどう業務に生かすかを考えてシステムの設計がなされている点がとても興味深いです.新商品に対する想定や,異常値が出ることも想定して,予測値を人間がチェックしてから発注するといったプロセスのように,実際的な Tips が良いです.

その他

巻末に PFR および提案書,PoC のドキュメントや結果報告書等含まれており,こうしたものは実業務に携わる人向けでよいなーと,他人事ながら思います.あとは機械学習の詳細を捨てて,システム屋が機械学習システムについて知る,というスタンスに特化したのは良い判断だよなーという感じです.そういう本は山ほどありますし,今更そういうのはいらないと思うので.

ただ細かいところでいくつか気になるところもあって,例えば参考書籍が初級本と中・上級本に極端に別れているところは,なんでこんな風になったんだろうという感がします(プロジェクトマネジメントに関する本で,PMBOK ガイドと「マンガでわかるプロジェクトマネジメント」しかないとか).あとHadoop に関する脚注が「Apach Spark のこと(原文ママ)」になっているのは,普通におかしいので修正いただけることを望みます*5.また細かい点だと,機械学習に関する用語はできるだけ平易な言葉遣いをされているんですが,後半にいきなり汎化性能という言葉が説明なく出てきたり,ローパス・ハイパスフィルタが説明なく使われていたり*6,細かいところでは気になる点があるのですが,こちらも修正されると良いなと思っています.

*1:この場を借りて厚く御礼申し上げます.

*2:個人的なスタンスとして,人工知能という言葉をできるだけ使いたくないので,本のタイトル等を除いては,このエントリでも機械学習で統一しています.

*3:こうした方の中には,精度100%でないならお前らのものは使わん! といったことをおっしゃる方もいらっしゃいました.まぁ機械学習をご存知の方ならお分かりの通り,精度 100% が達成できるなら,それははじめからルールベースで記述でき,機械学習をそもそも使う必要がないわけです...

*4:ここには,実のところ SIer のセールストークも多分に含まれます.もちろん商売なのでそういう行為は仕方がないわけですが,過度に期待を煽っても,実際に案件を受けるときに苦労するのは現場の人たちなのになぁ,と思うことはよくあります.

*5:Apach ではなく Apache だし,Hadoop は Spark とは全く別の技術です.

*6:どちらも一般的なシステムエンジニアが知っているわけではないように思います.

livy に接続するために Sparkmagic をインストール際にハマったところ

久しぶりに書きますが,すごい小ネタ.EMR 5.9.0 で livy0.4.0 がサポートされたので,ちょっと試してみようかなと思ったわけです.EMR のよくある問題は,Step 経由でジョブを投げると,ジョブを並列実行できないところで,この解消のために ssh でログインしてコマンド実行するとか,そんな感じのやり方をとる必要がありました.ここに livy があれば,REST でジョブを投げられるので,いろいろ捗るかな,というのが背景にあります.

そんなわけで,chezou さんの記事を参考に sparkmagic を入れて試してみました.

chezou.hatenablog.com

そうしたら pykerberos のインストールでこけて,??? となったんですが,結果的には以下の issue で挙げられている,libkrb5-dev を先にインストールすることで,無事に sparkmagic が入りましたよ,というお話.

github.com

使ってみると,REST API も非常にシンプルで使いやすそう.Jupyter からも接続できていい感じです.ただ現在のところ,sparkmagic から EMR クラスタの Hive Metastore につながらくて,なんでなのかがよくわかってないです.多分自分の設定が何か間違ってるんでしょうけど... 接続できると,Glue Data Catalog とも連携できていい感じなのに.

PySpark で書き出しデータのパーティション数を調節する

小ネタなんですが,なかなかググっても見つからず,あれこれと試行錯誤してしまったので,メモがわりに.

要するに,gzip 圧縮してあるデータを読み出して,年月ごとにデータをパーティション分けして,結果を parquet 形式の 1 ファイルで書き出す,みたいな処理がしたいということです.結局 repartition() を使えばよかったので,以下のように yyyymm カラムを一時的に作って,パーティションを切りなおしてからそのカラムを落とすというテクを使いました.普通なら repartition(int) で直接パーティション数を指定すれば良いんでしょうけど,複数年月が分けられておらず固まったデータを一気に読み込んで,一気にパーティショニングしたいときには,こんな感じで無理やり動かすしかないのかなーという感じです.ちなみに 8-9 行目の処理は,S3 への書き出しを高速化するための設定になります*1

ちなみに repartition() ではなく coalesce() を使うやり方もありますが,こちらの記事によるとファイルサイズが均一にそろわないっぽいです.また今回のとは別で,データサイズをみて適切なファイルサイズごとにパーティションを分けたい,という場合であれば,以下の記事にあるように df.inputFiles.size で取得できるんですね.

stackoverflow.com

from pyspark.context import SparkContext
from pyspark.sql import SQLContext
from pyspark.sql.functions import year, month, date_format

in_path = "s3://XXX/gz/*.gz"
out_path = "s3://XXX/parquet/"

sc._jsc.hadoopConfiguration().set("mapreduce.fileoutputcommitter.algorithm.version", "2")
sc._jsc.hadoopConfiguration().set("spark.speculation", "false")
sqlContext = SQLContext(sc)
sqlContext.setConf("spark.sql.parquet.compression.codec", "snappy")

originalDf = sqlContext.read \
    .format("com.databricks.spark.csv") \
    .option("delimiter", "\t") \
    .load(in_path)
renamedDf = originalDf \
    .withColumnRenamed("_c0", "prod_id") \
    .withColumnRenamed("_c1", "cust_id") \
    .withColumnRenamed("_c2", "time_id") \
    .withColumnRenamed("_c3", "amount_sold")
yearAddedDf = renamedDf.withColumn("year", year(renamedDf.time_id))
monthAddedDf = yearAddedDf.withColumn("month", month(yearAddedDf.time_id))
yyyymmAddedDf = monthAddedDf.withColumn("yyyymm", date_format(monthAddedDf.time_id, 'yyyyMM'))

repartitionedDf = yyyymmAddedDf.repartition("yyyymm")
dropedDf = repartitionedDf.drop("yyyymm")

castedDf = dropedDf.withColumn("prod_id", dropedDf.prod_id.cast("decimal(38,0)")) \
    .withColumn("cust_id", dropedDf.cust_id.cast("decimal(38,0)")) \
    .withColumn("time_id", dropedDf.time_id.cast("timestamp")) \
    .withColumn("amount_sold", dropedDf.amount_sold.cast("decimal(38,2)")) \
    .withColumn("year", dropedDf.year.cast("int")) \
    .withColumn("month", dropedDf.month.cast("int"))
castedDf.write.partitionBy(["year", "month"]).mode("overwrite").parquet(out_path)

*1:このあたりは,昨年の記事にまとめてあります.