ie_test
2018年7月22日日曜日
TFRecordフォーマット
# 目的 tensorflow を並列に使う方法として google cloud dataproc, google cloud dataflow や google cloud machine learning があるが、そういったサービスを受ける際に、gcs にデータを TFRecode フォーマットで置いたほうが良いという話がある。 その方が転送速度などの点では有利だという話がある。 とりあえず試してみた。 # csv から TFRecord フォーマットのファイルを作り、読み込む。 以下はコード tf_record.py ``` #coding:utf-8 import tensorflow as tf import numpy as np # csv から tfrecord フォーマットファイルへの書き込み. def write_test( fpath, result_fpath): with tf.python_io.TFRecordWriter( result_fpath ) as writer: with open( fpath, "r" ) as f: f.readline() # 読み捨て. for line in f: tmp_list = line.strip().split(",") tmp_f = map( int, tmp_list ) # generator のままで ok. example = tf.train.Example() example.features.feature["my_data"].int64_list.value.extend(tmp_f) writer.write( example.SerializeToString() ) return # tfrecord からの読み込み def load_test( fpath ): for i,record in enumerate(tf.python_io.tf_record_iterator(fpath)): example = tf.train.Example() example.ParseFromString(record) # バイナリデータからの読み込み tmp = example.features.feature["my_data"] a = tmp.int64_list.value print(i,a) fpath = "sample.csv" result_fpath = "test.tfrecords" write_test( fpath, result_fpath) load_test( result_fpath ) ``` tf.train.Example クラスのインスタンスに対して、 csv ファイルから一行づつ取り出しsplit したものを "my_data" として登録している。この際、 lint64_list.value に extent として突っ込んでいる。 このようにデータの型を明確にする必要がある。(なので文字列は使えないかもしれない。) 読み込む方法は tf.python_io.tf_record_iterator でレコード毎に取り出し、 int64_list.value として読み込み側で型を指定して取り出している。 データは次のもの sample.csv ``` a,b,c,d 1,2,3,4 9,10,11,13 9,7,11,3 ``` # 実行結果 ``` 0 [1, 2, 3, 4] 1 [9, 10, 11, 13] 2 [9, 7, 11, 3] ``` # 課題 このエンコード方法がどの程度有効なのか今の所、よくわからない。現状は実際の機械学習に乗せる感覚が掴めてないのでそこを考える必要がある。
python unittest について
python unittest について # 目的 メンテしやすいモジュール作成のためにテストを考えてみた。 特にエラー処理のテストなど、どう書くかが結構手間なものについて実装しておきたいと思った。 # ディレクトリ構成 . ├── data_make_tools │ └── train_data_make.py └── tests ├ ── test_train_data_make.py └── data └── weather └── test1.csv # コード 以下のようなファイルからデータを読み込んで辞書を返すようなコードを考える。 data_make_tools/train_data_make.py ``` def get_ym_to_city_to_weather_data( fpath ): ym_to_city_to_weather_data = OrderedDict() with open( fpath, "r") as f: f.readline() # ヘッダ読み捨て. for i,line in enumerate(f): try: year, month, city, \ cloudness, daylight_hours, precipitation_sum, \ temperature_max, temperature_min, temperature \ = line.strip().split(",") ym = YearMonth( int(year),int(month) ) if ym not in ym_to_city_to_weather_data: ym_to_city_to_weather_data[ym] = OrderedDict() ym_to_city_to_weather_data[ym][city] \ = WeatherData( cloudness, daylight_hours, precipitation_sum, temperature_max, temperature_min, temperature ) except : print( traceback.format_exc() ) print( "data file: " + fpath + ", line: %d"%( i+1 ) ) print( "data error line: " + line ) exit(1) return ym_to_city_to_weather_data ``` エラーが生じたときにちゃんとメッセージを出力しているのかのテストを書きたいのだが、 このままでは難しい。 # コード変更 logging モジュールを使って以下のようにコードを変更 data_make_tools/train_data_make.py ``` import logging THIS_MODULE_LOGGER = logging.getLogger(__name__) def get_ym_to_city_to_weather_data( fpath ): ym_to_city_to_weather_data = OrderedDict() with open( fpath, "r") as f: f.readline() # ヘッダ読み捨て. for i,line in enumerate(f): try: year, month, city, \ cloudness, daylight_hours, precipitation_sum, \ temperature_max, temperature_min, temperature \ = line.strip().split(",") ym = YearMonth( int(year),int(month) ) if ym not in ym_to_city_to_weather_data: ym_to_city_to_weather_data[ym] = OrderedDict() ym_to_city_to_weather_data[ym][city] \ = WeatherData( cloudness, daylight_hours, precipitation_sum, temperature_max, temperature_min, temperature ) except : THIS_MODULE_LOGGER.error( traceback.format_exc() ) THIS_MODULE_LOGGER.error( "data file: " + fpath + ", line: %d"%( i+1 ) ) THIS_MODULE_LOGGER.error( "data error line: " + line ) exit(1) return ym_to_city_weather_data ``` このようにモジュール毎にログ管理をするように変更する。 # テストコード tests/test_train_data_make.py ``` import os from unittest import TestCase import logging from io import StringIO from data_make_tools.train_data_make import get_ym_to_city_to_weather_data LOGGER = logging.getLogger("data_make_tools.train_data_make") class Test_get_ym_to_city_to_weather_data(TestCase): data_dir = os.path.join( DATA_DIR, "weather") def setUp(self): self.stream = StringIO() self.handler = logging.StreamHandler( self.stream ) LOGGER.addHandler( self.handler ) def tearDown(self): LOGGER.removeHandler( self.handler ) self.handler.close() def test_weather_1(self): fpath = os.path.join( self.data_dir, "test1.csv" ) self.assertRaises( SystemExit, lambda: get_ym_to_city_to_weather_data( fpath ) ) self.assertIn( "not enough values to unpack (expected 9, got 8)", self.stream.getvalue() ) ``` # テストコード説明 以下でログを流し込むオブジェクトを確保し、 data_make_tools.train_data_make の logger に登録. これはテスト実行前に必要なので setUp で行う。 ``` self.stream = StringIO() self.handler = logging.StreamHandler( self.stream ) LOGGER.addHandler( self.handler ) ``` 以下でテストする関数の実行と想定している例外が上がっているかをチェック ``` self.assertRaises( SystemExit, lambda: get_ym_to_city_to_weather_data( fpath ) ) ``` 以下で想定している文字列が出力されているかチェック. ``` self.assertIn( "not enough values to unpack (expected 9, got 8)", self.stream.getvalue() ) ``` 実行が終わったら tearDown によりオブジェクトを取り除き、元の状態にする。 ``` LOGGER.removeHandler( self.handler ) self.handler.close() ``` # テスト実行 ディレクトリのトップで以下のコマンドを実行. ``` python -m unittest discover tests ``` 上記は tests ディレクトリにある unittest のテストを全て見つけて、実行するというコマンドになっている。 python の unittest のカバレッジをとる. # カバレッジ カバレッジを見ることでテスト書くモチベーションになるんではないかと思って やってみた。 以下のテストについてカバレッジを計測したい。ディレクトリ構造なども以下と同様。 ## ツール準備 以下で coverage モジュールをインストール. ``` pip install coverage ``` ## coverage を使うために coverage を使うために以下のコードを用意. test.py ``` import unittest def exec_test_all(): all_tests = unittest.TestLoader().discover( "tests", "test_*.py") runner = unittest.TextTestRunner() runner.run( all_tests ) if __name__ == "__main__": exec_test_all() ``` 上記のコードは tests ディレクトリ以下の test_ で始まる python コードをテストコードとして 取って来るというコード. ## 実行方法 以下のコマンドでテスト実行. ``` coverage run test.py ``` 実行するとカレントディレクトリに .coverage というファイルが作られる。 (このファイルに実行結果が書き込まれる。) 以下のコマンドでカバレッジを可視化した html ファイルを生成. ``` coverage html --include=data_make_tools/* ``` --include=data_make_tools/* は data_make_tools 以下のコードに対してのみ カバレッジ結果を生成するという意味。 (こうしないとテストスクリプトの方のカバレッジも含んでしまう。) 実行すると htmlcov というディレクトリに html の表示のためのファイルが生成される。 htmlcov/index.html をブラウザで開いて結果を見る。
GCPをとりあえず使ってみた
# 0. 前準備 GCP のコンソールを使用したり gcloud コマンドを使うための準備が必要となる。 https://ietoa.blogspot.com/2018/07/gcp-google-cloud-platform.html にインストール方法などを書いた。 # 1. google cloud platform の「コンソール」という画面に移動 URL: https://console.cloud.google.com/home 「Compute Engine」->「VMインスタンス」と進む。 # 2. インスタンスを作る. 「インスタンスを作成」をクリック 以下の項目を設定 - 名前 : (なんでも良いがとりあえず watashi-gpu1 とする。) - ゾーン: us-central1-a - マシンタイプ: 「vCPU x 2」を選択して、「カスタマイズ」を選んで以下を設定. 「メモリを拡張する」にチェックを入れて、 - メモリ: 32 GB - CPUプラットフォーム: 自動 - GPU: - GPU の数 : 1 - GPU のタイプ : NVIDIA Tesla K80 - ブートディスク: 「変更」を選び、 - OSイメージ: Ubuntu 16.04 LTS - ブートディスクの種類: 標準の永続ディスク - サイズ : 40GB - ファイアウォール: 設定しない。 作るとすぐ起動した状態になる。使わない場合は「停止」すること。 # 3. 接続 「接続」の項目の右に下矢印があるのでそれをクリックし、 「gcloud コマンドを表示」をクリックすると以下のようなコマンド例が表示される。 ``` gcloud compute --project "プロジェクト名" ssh --zone "us-central1-a" "watashi-gpu1" ``` 上記を打ち込めば ssh として接続できる. 一回目は ssh の key の設定などをするが二回目はすんなり入れる。 ssh の鍵は ~/.ssh に ``` google_compute_engine google_compute_engine.pub google_compute_known_hosts ``` として登録される. # 4. ファイル転送 gcloud compute scp [LOCAL_FILE_PATH] [INSTANCE_NAME]:\~/ のフォーマットと書かれているが、 --project と --zone の指定が必要( gcloud コマンドで project, zone を設定すれば --project , --zone の記述はいらなくなるらしい。) ``` gcloud compute --project "プロジェクト名" scp --zone "us-central1-a" memo.txt watashi-gpu1:~/ ``` 上記のコマンドでローカルにある memo.txt ファイルをクラウド上の ~/ ディレクトリに転送することができる。 - 料金感覚 (GB 単位) 月間使用量 ネットワーク(下り)送信先: 世界 ネットワーク(下り)送信先: 中国 (香港を除く) ネットワーク(下り)送信先: オーストラリア ネットワーク(上り) (香港以外の中国とオーストラリアを除く) 0~1 TB $0.12 $0.23 $0.19 無料 1~10 TB $0.11 $0.22 $0.18 無料 10 TB~ $0.08 $0.20 $0.15 無料 アップは基本無料。 ダウンロードは 10 GB くらい落とすと 1.2ドルで 150 円くらいか. # 5. マシンの停止 ``` gcloud compute --project "プロジェクト名" ssh --zone "us-central1-a" "watashi-gpu1" ``` でマシンに入った状態で ``` sudo poweroff ``` と打ち込むと停止する。停止まで1,2分くらい時間がかかるので注意。 # 6. マシンの再起動 ``` gcloud compute --project "プロジェクト名" instances start --zone "us-central1-a" "watashi-gpu1" ``` 起動に 1,2 分かかる。 # 7. 自動実行停止 gcp にて計算が終わったら自動で停止させるには以下のようなスクリプトを用意する。 ``` run.sh python3 net_test.py >& log.txt sudo poweroff ``` このスクリプト run.sh に対して ``` $bash run.sh & ``` としてバックグラウンド実行すれば exit で ssh 接続を切っても走り続けて、計算が終われば自動で gcp のマシンを停止してくれるので経済的にも安心。 -
2018/4/12 修正
上記の方法だと回しているプログラムの種類によってはカーネルに殺されることもあるらしい。https://www.qoosky.io/techs/9dd96483ab (ssh を抜け出してもも大丈夫なことは何回か確認はしているが、念のため screen での動作を推奨したい。) なので以下のようなコマンドを打つ。 ``` screen -dm -S qoosky bash -c 'bash run.sh' ``` -S qoosky の部分はなんでも良い。 -c ' ' の ' ' の中に実行したいコマンド内容を書くこと。( -dm はデタッチメント(バックグラウンドジョブとして)で動かすということ。) ちゃんとスクリーンで動いているかは ``` screen -ls ``` とすることで確認可能。もしくは上記のスクリプトの場合は ``` cat log.txt ``` などとして出力がなされていることを確認すると良い。 screen で動いているプロセスを途中で止めたくなった場合には ``` screen -S qoosky -X quit ``` とすることで止めることができる。 -S qoosky の部分は動作させた時と同じものを指定する。 止まったかどうかは screen -ls で確認すると良い。
luigi をデータハンドリングに使ってみた
# 目的 データ分析をしていると、色々なデータを join( merge ) したり、 フィルターかけたり、一部のカラムだけ取ってきたりといった作業が多くなる。 そういった作業の末に作り上げたデータなどを作り直したり、また一部の工程だけを 違ったやり方にして実行するといったことを頼まれた時に四苦八苦してしまうことが多い。 luigi はそういった工程管理をスクリプト化することで管理しやすくする。 (少なくとも,そういった作業を簡易にしたいと思って開発されたものである。) # 他のツールなど 同じような目的のツールはビルドのためのツールが真っ先に思い浮かぶ。 make コマンドや python の scons など。 make はかなり汎用といえば汎用だが記述がシェルスクリプト前提であるため少し辛い。 scons は python で記述できるので競合たり得るかもしれない。 違いとしてはネットワーク越しの生成物のチェックや hadoop などの他ツールとの統合性が良さげらしい。 (あんまりピンとこないのでまたおいおい調べる必要があるだろう。。) 他にも色々あるかもだが著者の調査不足により、この議論はこれまでにとどめる。 # ツールインストール python のライブラリで, pip install luigi とすればインストールできる。 # ツールの簡単な使い方 以下は train_cust_item.csv, test_cust_item.csv という (キャストID,アイテムID) がレコードであるような csv ファイルから 正例、負例のレコードを作成し、必要なカラムを取ってきてジョインし、 学習用データと確認用データである train_data.csv, test_data.csv を作る工程を luigi で書いたものである。 (実際には train_cust_item.csv, test_cust_item.csv を作る部分も luigi でコード化しているのだが 説明には長すぎるのでそこはカット.) make_minus_data, make_fill_data, my_catenate は別途実装が必要. またかなりベタに書いてるのでもっと綺麗に書く方法があると思われる。 luigi_sample.py ``` class TestTrainSplit(luigi.Task): def output(self): return { "train_fpath" : luigi.LocalTarget("train_cust_item.csv"), "test_fpath" : luigi.LocalTarget("test_cust_item.csv") } class TestDataPlus(luigi.Task): def requires(self): return TestTrainSplit() def run(self): print("TestDataPlus") def output(self): return luigi.LocalTarget("test_cust_item.csv") class TrainDataPlus(luigi.Task): def requires(self): return TestTrainSplit() def run(self): print("TrainDataPlus") def output(self): return luigi.LocalTarget("train_cust_item.csv") class TestDataMinus(luigi.Task): def requires(self): return {"item_fpath": MakeItemData(), "cust_item_fpath": TestDataPlus() } def run(self): make_minus_data( self.input()["item_fpath"].path, self.input()["cust_item_fpath"].path, self.output().path ) def output(self): return luigi.LocalTarget("test_cust_item_minus.csv") class TrainDataMinus(luigi.Task): def requires(self): return {"item_fpath": MakeItemData(), "cust_item_fpath": TrainDataPlus() } def run(self): make_minus_data( self.input()["item_fpath"].path, self.input()["cust_item_fpath"].path, self.output().path ) def output(self): return luigi.LocalTarget("train_cust_item_minus.csv") class TestDataFill(luigi.Task): def requires(self): return {"test_data_plus_path": TestDataPlus(), "test_data_minus_path": TestDataMinus(), "user_data_path": MakeUserData(), "item_data_path": MakeItemData() } def run(self): make_fill_data( self.input()["user_data_path"].path, self.input()["item_data_path"].path, self.input()["test_data_plus_path"].path, self.output()["plus_out_path"].path, self.input()["test_data_minus_path"].path, self.output()["minus_out_path"].path ) def output(self): return {"plus_out_path":luigi.LocalTarget("test_cust_item_fill.csv"), "minus_out_path": luigi.LocalTarget("test_cust_item_minus_fill.csv") } class TrainDataFill(luigi.Task): def requires(self): return {"train_data_plus_path": TrainDataPlus(), "train_data_minus_path": TrainDataMinus(), "user_data_path": MakeUserData(), "item_data_path": MakeItemData() } def run(self): make_fill_data( self.input()["user_data_path"].path, self.input()["item_data_path"].path, self.input()["train_data_plus_path"].path, self.output()["plus_out_path"].path, self.input()["train_data_minus_path"].path, self.output()["minus_out_path"].path ) def output(self): return {"plus_out_path":luigi.LocalTarget("train_cust_item_fill.csv"), "minus_out_path": luigi.LocalTarget("train_cust_item_minus_fill.csv") } class MakeTestData(luigi.Task): def requires(self): return TestDataFill() def run(self): fpath_list = [ self.input()[input].path for input in self.input() ] my_catenate( fpath_list, self.output().path ) def output(self): return luigi.LocalTarget("test_data.csv") class MakeTrainData(luigi.Task): def requires(self): return TrainDataFill() def run(self): fpath_list = [ self.input()[input].path for input in self.input() ] my_catenate( fpath_list, self.output().path ) def output(self): return luigi.LocalTarget("tain_data.csv") class MakeTrainTestData(luigi.Task): def requires(self): return [ MakeTrainData(), MakeTestData() ] def run(self): print("MakeTrainTestData ok") ``` # コードの説明 通常、作業内容は「class MakeTrainTestData(luigi.Task)」のようにクラスで定義していく。 このクラスは三つのメソッドから成り立つ。 requires は、このタスクを開始する前に終わっておくべきタスクを指定する。 output はこのタスクの終了でどういったものが生成されるかを記述する。 私は細かい使い方がわからなかったので全てファイルを生成物として指定する使い方だけしている。 # 使い方 以下のコマンドで luigi_sample.py にある class MakeTrainData の output で指定されている "train_data.csv" が生成されるまでの作業が全て実行される。 ``` PYTHONPATH="." luigi --local-scheduler --module luigi_sample MakeTrainData ``` ただし、依存途中で指定されているファイルがある場合は、そのファイルの生成を行う class の run メソッドは実行されない。 もしくは 他のタームで $ luigid とすると localhost:8082 で luigi のデーモンが動く. すると --local-scheduler の部分を省略しても実行できる。 ``` $ PYTHONPATH="." luigi --module luigi_ver5 MakeTrainTestData ``` これはデーモンに処理を投げるようになるということである。 このように実行すると実行の詳細な情報をブラウザで確認できる。
GCP (google cloud platform)を使うまで
# 概要 GCP を使うことになったのでコマンドの導入などをメモ。 環境は mac によって行った。 # google cloud sdk のインストール gcloud、gsutil、bq コマンドを使えるようにするために google cloud sdk をインストールする. 以下のリンクから google-cloud-sdk-180.0.0-darwin-x86_64.tar.gz をダウンロードして解凍する。 https://cloud.google.com/sdk/?hl=ja $./google-cloud-sdk/install.sh としても python が 2.7 でないとインストールエラーとなる。 なので anaconda ならば以下のようなコマンドを打って、python2.7 の環境を作る。 $conda create -n py27 python=2.7 anaconda python2.7 環境で実行したい場合には以下のようにコマンドをうつ。 $source activate py27 するとプロンプトが以下のような感じになる。 (py27) bash-3.2$ これで $./google-cloud-sdk/install.sh でインストール可能となる。 インストールが済めば別に python2.7 でなくても良いので上記の環境を抜けて良い。 そのあと、どう使うかよくわからなかったが、要は PATH が通って入れば良いので ~/.bashrc に export PATH=$PATH:/Users/watashi/Desktop/GCP/google-cloud-sdk/bin といった記述を追加して( watashi はmac 上のユーザー名。) google-cloud-sdk/bin にパスを通す. パスが通っているかは $which bq などとして確認すること。 # アカウントの設定 GCP を使うにはクラウドにログインするアカウントを設定する必要がある。 以下のようにメアドでアカウントを設定. $gcloud config set account [メールアドレス] 以下のコマンドを打つと、ブラウザが開き、認証設定をするためのページになるので「許可する」とかをクリックしていけば ok. $gcloud auth login # アカウント設定完了の確認 以下を打つとテーブルの情報が見れる。 $bq show プロジェクト名:テーブル名 # python からのアクセス - 以下でまずはライブラリをインストール。 $pip install pandas-gbq - 以下をブラウザで開く。 https://console.developers.google.com/apis/dashboard 「ライブラリ」のところから辿っていき「BigQuery API」をクリックする(結構下の方にある)。 「BigQuery API」の画面になるので「有効にする」をクリックすればよし。 - 次に以下をブラウザで開く。 https://console.cloud.google.com/home/dashboard 「IAMと管理」のメニューから「サービスアカウント」を選択. 「キーを作成」で「json」を選ぶ。 するとキーが生成され、その情報が「392t2taastw0.json」といったファイルとしてダウンロードされる。 このファイルのあるディレクトリにおいて以下のコードを実行すれば普通の pandas のように実行できる。 ``` import pandas query = 'SELECT * FROM [プロジェクト名:テーブル名.ファイル名]' project_id = 'プロジェクト名' private_key = '上記で取得した jsonファイル名' data_frame = pandas.read_gbq(query, project_id=project_id, private_key=private_key) print(data_frame) ``` private_key のところはフルパスを設定してもいけるので jupyter notebook からの実行のようにカレントがわかりずらい場合にはそうした方が良いかもしれない。 # 料金 以下を見てみると「クエリ: $5/TB (スキャンしたデータのサイズで課金)」とある。 https://qiita.com/kaiinui/items/3ae3a335a2826fc7070a 返すデータのサイズではない。カラム指向なので select * とはせずに返す列を指定した方が良いとのこと。 https://www.slideshare.net/dragan10/20150520-gcpug-osaka-big-query またインデックスは基本的に考えなくて良いらしい。 # 注意 bq コマンドでファイルをアップする際には各カラムの型について予測していい感じで登録してくれる。 ただし、 20170912 のような日付の数値も integer となるのでそこは各自良い感じで対応すること。
xlsxwriter でエクセル資料作り
# 目的 レコメンドの結果としてレコメンドアイテムの写真を貼り付けた資料などを作ることがある。その際に python の xlsxwriter モジュールが使えるかなということで紹介。 # コード説明 説明は以下のコードの下に記載。 ``` #coding:utf-8 import os # need pip install. import xlsxwriter # PIL install command ( $pip install pillow ). import PIL.Image as img def get_image_size( image_path ): image_data = img.open( image_path ) size = image_data.size image_data.close() return size def excel_write(): result_fpath = "test.xlsx" sheet_name = "item_seet" wb = xlsxwriter.Workbook( result_fpath ) ws = wb.add_worksheet( sheet_name ) # vba と違ってインデックスは 0 始まりなことに注意せよ. ws.set_column( 0, 11, 28 ) # 0 列目から 11 列目までの横幅を 28.0 に設定 x_cell_len, y_cell_len = 200, 200 row_cell_num = 12 # 画像の分 + その画像の説明文字列の分を行としてとる。 col_offset = 1 for cust_i, mem_id in enumerate(mem_id_list): # 会員番号がそのままディレクトリ名になっている。 pic_dir = os.path.join( data_dir, mem_id ) row = cust_i * row_cell_num ws.write( row, 0, mem_id ) image_row = row + 1 for item_j, fpath in enumerate(os.listdir(pic_dir)): image_path = os.path.join( pic_dir, fpath ) x_image, y_image = get_image_size( image_path ) x_scale, y_scale \ = float(x_cell_len)/x_image, float(y_cell_len)/y_image col = item_j + col_offset ws.write( row, col, fpath ) ws.insert_image( image_row, col, image_path, {'x_scale': x_scale, 'y_scale': y_scale } ) wb.close() ``` まず出力するエクセルファイルを wb = xlsxwriter.Workbook( result_fpath ) で作成. 書き込むエクセルシートをws = wb.add_worksheet( sheet_name ) で追加。 ws.write( row, col, s ) : ws シートの行 row, 列 col に文字列 s を書き込む( row, col は vba と違って 0 始まりなので注意せよ。 ws.insert_image( row, col, image_path, {'x_scale': x_scale, 'y_scale': y_scale } ) : ws シートの行 row, 列 col を左上として image_path にある画像ファイルを貼り付ける。 x_scale, y_scale はそれぞれ x, y 方向に元の画像の何倍にするかを設定できる。 最後に wb.close() でブックを閉じるとファイルの書き込みが確定する。 # 課題 もともとあるエクセルファイルを開いて編集などは難しいのではないかと思う。
python の最適化ツール ortools について
# 最適化問題設定 各ユーザーにアイテムを勧めたい。条件として各ユーザーには 4 つのアイテムを勧める。各ユーザーがそのアイテムを好むかどうかはスコアとして数値化されており、選んだアイテムのスコアの合計をできるだけ大きくしたい。 ただし、アイテムはカテゴリー毎に分類されており、一つ目に勧めるアイテムが属するカテゴリー、二つ目に勧めるアイテムが属するカテゴリー、. . . 四つ目に勧めるアイテムが属するカテゴリーは決められている。 また各カテゴリーの各価格帯に対してはユーザーに勧める個数の割合が決まっており、その割合から一定の範囲に収めなければならない。 # 数式による定式化 $u = 1, \dots, U$ をユーザーを表す添字 $n = 1, \dots, N$ をレコメンドの順番を表す添字(上記の問題設定では $N=4$ ) $c = 1, \dots, C$ をカテゴリーを表す添字 $p = 1, \dots, P$ を価格帯を表す添字 とする。 - 変数 $v_{u,n,c,p} \in \{ 0, 1 \}$ 上記は0-1整数変数を意味している。 変数の意味は $v_{u,n,c,p} $ が 1 の時は、「ユーザー u に対する n 番目のレコメンドでカテゴリー c の価格帯 p に属するアイテムを選ぶ」ということである。 - 目的関数 $s_{u,c,p} $ によってユーザー u のカテゴリー c、価格帯 p に属するアイテムを選んだ際のスコアを表す。この最適化問題においては定数として取り扱う。 目的関数はスコア合計ということで $\sum_{u,n,c,p}{s_{u,c,p}\cdot v_{u,n,c,p}}$ とする。 (実際にレコメンドをする人の感覚にあっているかどうかは難しい問題なのでここでは問わない。業務では重要だが。線形でないと整数計画問題はなかなか解けないという理由もある。) - 制約 二種類の制約がある。 - 一度に勧められるアイテムは一つだけ $\sum_{c,p}v_{u,n,c,p} = 1, \forall u \forall n$ - 出現割合をある一定の範囲にする $r_{c,p} \in [0,1]$ をカテゴリー c、価格帯 p の希望出現割合を表す定数とする。 $\rho \in [0,1]$ を許容割合を表す定数とする。 $ (1-\rho)\cdot r_{c,p}\cdot U\cdot N \le \sum_{u,n}v_{u,n,c,p} \le (1+\rho)\cdot r_{c,p}\cdot U\cdot N, \forall c \forall p$ r が割合なのでレコメンド総数を表す $U\cdot N $ を掛けている。 - まとめ Maximize $\sum_{u,n,c,p}{s_{u,c,p}\cdot v_{u,n,c,p}}$ such that $\sum_{c,p}v_{u,n,c,p} = 1, \forall u \forall n$ $ (1-\rho)\cdot r_{c,p}\cdot U\cdot N \le \sum_{u,n}v_{u,n,c,p} \le (1+\rho)\cdot r_{c,p}\cdot U\cdot N, \forall c \forall p$ # ortools による記述 - 最適化を解いてくれるソルバーを生成 ソルバーの生成 ``` import ortools # ソルバー生成 from ortools.linear_solver import pywraplp solver = pywraplp.Solver('Select', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING) ``` - 変数を設定 先ほど生成した solver のメソッドを使って生成する。引数の 0, 1 はそれぞれ変数の下限、上限を表す、三つ目の引数はデバッグ用に変数につける名前。 variables という変数に保存しているがこれは後々制約式、目的関数を作るのに参照が必要になるからである。 変数設定 ``` variables = OrderedDict() # 変数を設定 for u in u_c_p_to_score: variables[u] = OrderedDict() for n in n_to_cats: variables[u][n] = OrderedDict() for c in n_to_cats[n]: variables[u][n][c] = OrderedDict() for p in u_c_p_to_score[u][c]: variables[u][n][c][p] = solver.IntVar(0, 1, 'intVar(%s,%d,%d,%d)'%(u,n,c,p) ) ``` - 目的関数を設定 u_c_p_to_score は $s_{u,c,p}$ を表す辞書。 n_to_cats は n 回目に選ぶカテゴリーのリストを表す辞書。 solver.Maximaize で最大化する式を設定する。 目的関数設定 ``` obj_terms = [] for u in u_c_p_to_score: for n in n_to_cats: for c in n_to_cats[n]: for p in u_c_p_to_score[u][c]: s = u_c_p_to_score[u][c][p] v = variables[u][n][c][p] obj_terms.append( s * v ) # solver.Sum が list の要素を足し合わせた式を返す。 obj = solver.Sum( obj_terms ) solver.Maximize( obj ) ``` - 制約設定1 不等式 cons を作るだけでは制約は設定されない。 solver.Add( cons ) とすることで問題の制約が設定される。 一つだけ選ぶ制約の設定 ``` for u in variables: for n in variables[u]: select_cons_terms = [] for c in variables[u][n]: for p in variables[u][n][c]: v = variables[u][n][c][p] select_cons_terms.append(v) cons = solver.Sum( select_cons_terms ) == 1 solver.Add( cons ) ``` - 制約設定2 c_p_w は $r_{c,p}$ rho は $\rho$ r_num は $U\cdot N $ 希望出現割合の制約の設定 ``` for c in c_p_w: for p in c_p_w[c]: rate = c_p_w[c][p] cons_terms = [] for u in variables: for n in variables[u]: if c not in variables[u][n]: # ユーザーが選べないカテゴリーは飛ばす。 continue if p not in variables[u][n][c]: # ユーザーが選べない価格帯は飛ばす。 continue cons_terms.append( variables[u][n][c][p] ) z = solver.Sum(cons_terms) lb = math.floor( (1-rho) * rate * r_num ) ub = math.ceil( (1+rho) * rate * r_num ) cons_lb = z >= lb cons_ub = z <= ub solver.Add( cons_ub ) solver.Add( cons_lb ) ``` - 求解と解出力 sover.Solve() で解く。 v.SolutionValue() で解いた答えの値を得る。 求解と解出力 ``` solver.Solve() for u in variabels: for n in variables[u]: for c in variables[u][n]: for p in variables[u][u][c]: v = variables[u][n][c][p] vv = int(v.SolutionValue()) if vv == 1: print( u,n,c,p ) ```
pyOpt をmac上のdockerにインストールする
# 目的 最適化部分について pyopt が必要になったが mac だとインストールできるかわからない。 (pyopt のページを見ても mac については記述なしで、そもそもlinux でもインストールがめんどいのにさらに苦労するだろうことが予想される。) # どうしたか 結局 docker に ubuntu を入れて、インストールした. # docker 準備 docker のインストールに関しては下記を参照。 https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81 # 実行したこと まずdocker でコンテナを作成する。 ``` docker run -p 5901:5901 -it --name ubuntu_for_pyopt ubuntu:16.04 /bin/bash ``` 上記のコマンドを打つとコンテナが作られ、開始して、すでにコンテナの中にいる状態となる。以降は pyopt のインストールのためのコマンド。 ``` apt-get update apt-get install python3-pip apt-get install git pip3 install --upgrade pip # gfortran のインストール. pyOpt のインストールに必要. apt-get install gfortran # swig のインストール. pyOpt のインストールに必要. apt-get install swig # numpy がなぜか入っていない.あとなぜか pip3 は動かないので普通の pip を使う。 pip install numpy git clone https://github.com/madebr/pyOpt.git madebr_pyOpt cd madebr_pyOpt python3 setup.py install ``` アップデートなどここまで 5~10 分くらいかかる。 # 確認 ひとまず exit で出る. 以下のファイルを用意し、コンテナに送る。 tmp.py ``` #coding:utf-8 import pyOpt import numpy as np def objfunc(x): f = -np.prod(x) g = np.zeros(2) g[0] = x[0] + 2.0 * x[1] + 2.0 * x[2] - 72.0 g[1] = -x[0] - 2.0 * x[1] - 2.0 * x[2] fail = 0 return f, g, fail def main(): opt_prob = pyOpt.Optimization("hoge", objfunc) opt_prob.addObj("f") opt_prob.addVar("x1", "c", lower = 0.0, upper = 42.0, value = 10.0) opt_prob.addVar("x2", "c", lower = 0.0, upper = 42.0, value = 10.0) opt_prob.addVar("x3", "c", lower = 0.0, upper = 42.0, value = 10.0) opt_prob.addConGroup("g", 2, "i") print(opt_prob) slsqp = pyOpt.SLSQP() slsqp.setOption("IPRINT", -1) [fstr, xstr, inform] = slsqp(opt_prob, sens_type = "FD") print(opt_prob.solution(0)) print("hello,pyopt") main() ``` コマンドは次の通り ``` docker cp tmp.py 340d9c43bb23:/ ``` ちなみにコンテナから逆にファイルをコピーするときは以下のようなコマンド ``` docker cp 340d9c43bb23:/tmp.py . ``` ここで 340d9c43bb23 は作ったコンテナの ID. コンテナのIDは以下のコマンドで確認する。NAMES のところが ubuntu_for_pyopt となっている行を探せば良い。 ``` docker ps -a ``` ちなみに docker ps だと動いているコンテナ、 docker ps -a だと動いていないコンテナも含めて全て表示される。 以下で python 実行 ``` docker exec -it ubuntu_for_pyopt python3 tmp.py ``` 「docker exec -it ubuntu_for_pyopt /bin/bash」でコンテナに入ってから実行でも良いが、mac から直接呼び出すことが増える時には上記のコマンドのが良さげ. 使い終わったら以下のコマンドで止める。 ``` docker stop ubuntu_for_pyopt ``` 止まっているかどうかは「docker ps -a」で確認。 次に使う時には以下のコマンドでコンテナを起動する。 ``` docker restart ubuntu_for_pyopt ``` # 追記(2018/5/17) docker cp について、コンテナID の指定でなくてコンテナネームの指定でもよい。 ``` docker cp ubuntu_for_pyopt:/tmp.py . docker cp tmp.py ubuntu_for_pyopt:/ ``` 実行について以下のように環境変数を設定して実行できる。 問題が生じたときは以下のように実行する必要があるだろう。 ``` docker exec -it -e LANG=ja_JP.UTF-8 ubuntu_for_pyopt python3 tmp.py ```
pyOpt の slsqp について
# 動機 下記のような最適化問題において、python のモジュールである pyOpt の slsqp アルゴリズムを使用してみた。 - 最適化問題 予測機を使って、仕入数から販売数を予測しながら、仕入の各店への配分を最適化する。 指標としては各店において、できる限り仕入数と販売数の差分が小さくなるように最適化する。 # 定式化 - 定数 総仕入数: $total\\_num$ 店舗数: $n$ 最低仕入数: $lower$ 各店舗のデータ: $df_i \in \mathbb{R}^k$, ( $k$ はデータの次元、$i$ は各店舗を表す添字 ) - 最適化定式 - 変数 : $x_i \in \mathbb{R}, i \in \{ i: 1 \leq i \leq n \} , ( 0 \leq x_i \leq 1.0 )$ - 補助定義 各店舗の仕入数: $y_i = total\\_num*x_i$ 販売数を出力する予測機 (機械学習などで学習して得た予測機): $pred : \mathbb{R}^k \times \mathbb{R} \to \mathbb{R} $ - 目的関数: 最小化 $ \sum_i { | y_i - pred( df_i,y_i ) |^2 )} $ - 制約: $ \sum_ix_i = 1$ ( $x_i$ が比率であることを意味する制約) $ y_i >= lower $ ( 仕入数は最低仕入数以上) # 上記問題の取り扱いにおける問題 予測機 pred は通常、数式的に与えられないため微分値を得るのが困難であり、最適化しずらい。 一般にこの種の問題の取り扱い方は二つある。 1. 数値微分を使う。 2. 関数の形を推論し、その推論した関数に対して最適化を行う。 1 の方が微分情報についてより直接的なので誤差は少ないと考えられる。ただし、ハイパーパラメータチューニングなどの場合、 そもそも関数の値を得るための計算時間が莫大になるために 2 の方法が選ばれる傾向にある。python の hyperopt モジュールは 2 の方法を取っている。 pyOpt のアプローチは 1 の「数値微分」を取り扱う方である。xgboost で学習した pred の出力は比較的速いので有効と思われる。 数値微分をしているところについては本ブログの「pyOpt の微分情報について」を参照せよ。 # pyOpt のコード例 上記の定式化に対するコードを以下に掲載 ``` import numpy as np import pyOpt class FormulationForPuchaseOptimize: def __init__(self, model, df, purchase_total_num, lower, x_len ): self.model = model # 予測モデル. self.df = df # 各店舗のデータカラム. self.purchase_total_num = purchase_total_num # アイテムの全ての店舗の仕入数合計 self.lower = lower # 各店舗の最低仕入数. self.x_len = x_len slsqp = pyOpt.pySLSQP.SLSQP() # 1 にすると、multi process でやるときファイルエラーになる時がある. slsqp.setOption( "IPRINT", -1 ) self.slsqp = slsqp pass def create_formulation_prob( self ): # pyopt のインターフェイスに合わせるために下記のような関数を返すメソッドを用意した。 # (定式化に用いるパラメータを引数以外で関数に埋め込むため.) def get_func_value( x ): # x は 1 次元の np.array を想定 y = x * self.purchase_total_num # 割合が x の時の各店の仕入数. self.df[ "purchase_num" ] = pd.Series( y, index=self.df.index ) # out: 予測販売数 out = self.model.predict( self.df.values ) # ここが解析的にできない部分. f = mean_squared_error( out, y ) # 差分を目的関数とする. #f = mean_absolute_error( out, y ) # 差分を目的関数とする. # 等式、不等式制約を設定 g = np.zeros( 1 + self.x_len ) # 等式制約( == 0となるように記述する) g[ 0 ] = np.sum(x) - 1.0 # 合計=1, x が割合を表すようにするための制約. g[ 1: self.x_len+1 ] = self.lower - y # lower <= y, y は 1 以上という制約. fail = 0 return f, g, fail return get_func_value def prepare( self, seed ): # 変数初期化のための乱数シードの設定. np.random.seed( seed ) random.seed( seed ) opt_prob = pyOpt.Optimization( "purchase optimize", self.create_formulation_prob() ) # 変数の設定 x0 = np.random.rand( self.x_len ) # 0 ~ 1.0 の間の初期値をてきとうに選ぶ。 opt_prob.addVarGroup( 'x' , self.x_len, type='c', value=x0, lower=0.0, upper=1.0 ) # 販売予測値と仕入数の差分の最小化を目的関数とする。 opt_prob.addObj( "diff_pred_and_purchase" ) # sum(x) == 1 に相当する式の登録. opt_prob.addConGroup( "sum_x == 1", 1, "e" ) # lower <= y に相当する制約の登録 opt_prob.addConGroup( "lower_y", self.x_len, "i" ) return opt_prob def solve( self ,seed=1 ): # 定式化準備 opt_prob = self.prepare( seed ) [f_opt, x_opt, info] = self.slsqp( opt_prob, sens_type = "FD" ) if info['value'] != 0: print( "error! in %s", __name__ ) return None, None return f_opt, x_opt ``` create_formulation_prob は「 numpy.array である x が引数で渡された時に目的関数値 f と制約式の数値 g, 失敗したかどうかの数値 fail を返す関数」を返す関数である。 - なぜこんな面倒なことをしているかについての余談 slsqp のインターフェイスに合わせるためには引数は変えられない。引数以外からこの関数に対して、purchase_total_num のようなパラメータを埋め込みたい。python のように関数型言語的な機能がある場合、関数を返す関数で実装することになる(このように返された関数をクロージャとか呼んだりする)。 オブジェクト指向的なやり方ならば、そもそもインターフェイスとして slsqp クラスを継承して実装させるようにする。 普通の C 言語の場合、グローバル変数、スタティック変数、もしくは void* を引数に持たせて、内部で実装者がキャストして使うといったやり方になる。 このクラスのインスタンスを使用するコードは以下の通り ``` total_purchase = 30 lower = 1 with open( model_save_fpath, "rb" ) as f: model = pickle.load(f) df = pandas.read_csv( data_fpath, encoding="utf-8" ) optimizer = FormulationForPuchaseOptimize( model, df, total_purchase, lower, len(df) ) f_opt, x_opt = optimizer.solve() print(x_opt) ``` # 計算時間の目安 - 環境 下記の環境の docker 上で実行。 - 機種ID: MacBookPro14,2 - プロセッサ名: Intel Core i7 - プロセッサ速度: 3.5 GHz - プロセッサの個数: 1 - コアの総数: 2 - 二次キャッシュ(コア単位): 256 KB - 三次キャッシュ: 4 MB - メモリ: 16 GB - 計測結果 概ね 100 問に対して 15 秒程度 # pyOpt の実装 上記のコード例からもわかるように pyOpt では微分情報をどのように取っているのか明確ではない。pyOpt の実装を見ることでどのようにしているかがわかる。 - ソースコード ソースコードは svn コマンドにより次のようにして取ってくる。 ``` svn co http://svn.pyopt.org/trunk pyOpt ``` slsqp のメインとなるソースは pyOpt/pySLSQP にある。 pyOpt/pySLSQP/source ディレクトリにある fortran コードをコンパイルしたものを slsqp モジュールとし、pyOpt/pySLSQP.py から呼んでいるといった形式である。 - アルゴリズム 流れとしては初期点 x から目的関数値、制約式の数値、目的関数の微分値、制約式の微分値を用いて、 その周りの目的関数と制約式をそれぞれ二次関数、一次関数で近似した QP(quadratic programming) を解き、 変分 dx を決める。x = x + alpha* dx で x を更新。 といった形で、更新を進めていき、ある種の収束条件を満たしたら終了とする。 収束条件や alpha の詳細については以下の文献(ネットに落ちてる。)の chapter 2 による。 「Kraft D (1988) A software package for sequential quadratic programming. Tech. Rep. DFVLR-FB 88-28, DLR German Aerospace Center — Institute for Flight Mechanics, Koln, Germany.」 ただ、今だったら「Jorge Nocedal Stephen J. Wright, Numerical Optimization」(なぜかネットに落ちている。。)の chapter 18 にある 「Line search SQP method」を参考にしたほうが良いと思われる。 ちなみに nlopt の slsqp でも同様のソースを基にしている。 https://github.com/stevengj/nlopt/tree/master/src/algs/slsqp - QP について QP のアルゴリズム(lsq)は、「LAWSON & HANSON: SOLVING LEAST SQUARES PROBLEMS」(amazonで購入可能)という本に載ってるものそのまま。 この部分は40年くらい特に何もない。 # pyOpt の微分情報について 以下のようなコードで数値微分を行っている(説明の都合上、簡略化している)。デフォルトは sens_step = 1.0e-6 となっている。 pyOpt/pyOpt_gradient.py の class Gradient の getGrad メソッド ``` dh = sens_step xs = x k = 0 for i in mydvs: xh = copy.copy(xs) xh[i] += dh # ... 略 [fph,gph,fail] = opt_problem.obj_fun(xh, *args, **kwargs) if isinstance(fph,float): fph = [fph] for j in xrange(len(opt_problem._objectives.keys())): dfi[j,k] = (fph[j] - f[j])/dh for j in xrange(len(opt_problem._constraints.keys())): dgi[j,k] = (gph[j] - g[j])/dh k += 1 ``` # エラーが返されたのでデバッグ 店舗数が 30 以上といった場合に解けないということが生じていた. [f_opt, x_opt, info] = self.slsqp( opt_prob, sens_type = "FD" ) の info["text"] を見ると原因として、「Iteration limit exceeded」と出ている。 対処法としては以下の方法が考えられる。 1. 初期値を変えつつ何度もトライする。 2. 初期値を全て 1/total_num のように制約を満たすものにする。 3. slsqp.setOption( "MAXIT", 100 ) のようにイテレーション回数を増やす(デフォルトは 50) 4. slsqp.setOption( "MAXIT", 1.0e-4) のように判定誤差を甘くする。(デフォルトは1.0e-6 ) 5. sens_step(デフォルト 1.0e-6) を変える。(これは数値微分をとるときのステップサイズ) 6. 定式化を変える。 以下のように等式制約を変数にしてみた。 - 変数 : $x_i \in \mathbb{R}^{n-1} , ( 0 \leq x_i \leq 1.0 )$ - 補助変数(コード上では定義しない。) $ x_n = 1 - \sum_i(x_i) $ - 各店舗の仕入数: $ y_i = total\\_num*x_i$ - 目的関数: 最小化 $ \sum_i | y_i - pred( y_i ) |^2 $ - 制約: $\sum_{i\leq n-1}x_i \leq 1 $ ( $x_i$ が比率であることを意味する制約) $y_i \geq lower$ ( 仕入数は最低仕入数以上) - 結果 1,3,4 はどれも有効。現実としてはトライのたびに 3 のようにイテレーション回数を増やすといった実装が良いのではないか。 2 は試してないが解を偏らせるのではないかと心配。 4 も解を甘くしてしまうのは気になる所。 5 は試していない。predの感度に合わせてチューニングするのが良い。 6 は思ったよりうまくいかなかった。「LDL 分解する際に正定値でない」とかそんなエラーが生じる。
指数分布
以下を参考にしました。 https://mathtrain.jp/expdistribution # 分布 $f(x) = \frac{ exp(-\frac{x}{\mu}) }{ \mu} , x>0$ 期待値 $\mu$ 分散 $\mu^2$ # 使われ方 指数分布は「ある現象がある回数発生するまでの期間の長さ」に関する分布に対して用いることが多い。 理由は以下に書く「導出方法」を参照せよ。 ちなみに「ある一定期間に発生するある現象の回数」に関する分布に対してはポアソン分布が有名。 # 導出方法 $f(x)$ を時期 $x$ において初めて発生確率の密度関数とする。 定義より$x $ から$x+\delta x$ の間に初めて事象が発生する確率は$f(x)\delta x$と考えられる。 一方で上記の確率は、 (時刻 $x$ までイベントが起こらない確率)$\times$(それから $\delta x$の間に起こる確率) であり、時期によらず、考慮期間に比例して発生する確率が上がるとすれば $(1 - \int_0^xf(x)dx)\times \lambda \delta x$ と考えられる。($\lambda$は比例定数とする。) よって $(1 - \int_0^xf(x)dx)\times \lambda = f(x)$ という積分方程式を得る。 この式を両辺 $x$で微分すると、 $-\lambda f(x) = f'(x)$ これを解くと $ f(x) = Ce^{-\lambda x }$ で積分して 1 になるとすれば、 $ f(x) = \lambda e^{-\lambda x }$ となり、$\lambda = \frac{1}{\mu} $ となることがわかる。
rust の勉強メモ
# emacs 環境の準備 以下を参考にした. https://github.com/rust-lang/rust-mode 以下を .emacs に書き込む ``` (require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) (package-initialize) ``` M-x package-install とすると何をインストールするか聞かれるので、 rust-mode と入力. これで .rs のファイルを開くと rust-mode として動作する。 # rust のドキュメント第二版を紹介 rustのドキュメントの第二版が良い感じだったので紹介 https://doc.rust-lang.org/book/second-edition ## 良いと思ったところ ボローイングとライフタイムについての説明がだいぶ良くなっていると感じた。 ### ボローイング https://doc.rust-lang.org/book/second-edition/ch04-02-references-and-borrowing.html 第一版だと例に使ってたのが i32 で、Copy がデフォルトの動作をしてしまうため、ボローイングが成されていることが非常にわかりづらかった。String のオブジェクトを例にしたのはわかりやすくなっていると思う。 ### ライフタイム https://doc.rust-lang.org/book/second-edition/ch10-03-lifetime-syntax.html 引数のオブジェクトのライフタイムが違う例が紹介されていてライフタイムの考えがわかりやすくなっている。 引数のライフタイムが実際には違うが同じライフタイムシンボルで定義されている場合には短いライフタイムに合わせられるなど、こちらを読んで理解が追いついた。
Blogger のカスタマイズ
# 概要 使いづらかったので以下の点でカスタマイズ 1. リアルタイムプレビュー 以下のサイトに従った。 http://nervouskidkitten.blogspot.com/2016/11/blogger_5.html 2. markdow 対応 以下のサイトに従った。 https://qiita.com/her0m31/items/1804bdc251a647e0e9a8 2018/7/21 現在では「テンプレート」という項目に代わって「テーマ」という項目になっている。それ以外はサイトの通り。 3. tex 対応 http://irrep.blogspot.com/2011/07/mathjax-in-blogger-ii.html 上記のサイトに従って以下のコードを HTML の先頭に差し込むようにした。 ``` ``` # 課題 1. リアルタイムプレビューが意外と重たくて安定しない。 2. プログラムコードをどうするか。現状は バッククォート 3 つでくくるというやり方をしている。 これだとjavascriptのコードでも評価されないでそのまま表示されるのでとりあえずは良いと思う。
レコメンドの評価指標
# 概要 https://arxiv.org/abs/1806.01973 で使用していた評価指標について述べる。 指標は二つ提示されていて、一つは「hit-rate」,もう一つは「MRR」とよんでいる。 # 設定 1. 観測データとして $(q,i)$ のペア群が与えられたものとして評価する。 ここで $q$ はユーザー、$i$ は薦めるアイテムを想定していて、$(q,i)$ は実際にユーザー $q$ がアイテム $i$ を購入(もしくはクリック) した場合に、このペアが観測されるものと想定している。観測される $(q,i)$ ペア全体の個数を $n$ とする。 2. 想定しているレコメンドシステムは、各ユーザー $q$ に対するアイテム $i$ のスコア $s(q,i)$ が計算されるものとし、このスコア $s(q,i)$ が現実に即しているかどうかを評価したい。 # hit-rate レコメンドシステムにおいて各 $q$ に対してスコア $s(q,i)$ が上位のものから順に、 $K$ 個のアイテムをとってくる。 この集合を $I_{q,K}$ とした時、 $$hit-rate = \frac{1}{n}\sum_q | I_{q,K} |$$ と定義する( $| I_{q,K} |$ は集合 $I_{q,K}$ の要素の個数)。これは $K$ に依存した評価であり、$K$ は実際にレコメンドする個数に依存して決めるのが良いと思われる。(ちなみに論文においては $K=500$ としている。) hit-rate の値が大きいほど、スコアと実際の購入(もしくはクリックなど)の結びつきが強いと評価できる。 # MRR $R_{q,i} $はスコア $ s(q,i) $ のユーザー $q$ における順位とする。 この時、 $$MRR = \frac{1}{n}\sum_{(q,i)}{ \frac{1}{\lceil R_{q,i}/K \rceil} } $$ と定義される。 順位が上がれば上がるほど(つまり数値が少なければ少ないほど)、MRR は大きくなる。 この指標は $K$ に依存した指標であり、$K$ はどの程度の順位幅は同じとみなすかというパラメータである。(論文では $K=100$ としている。) MRR の値が大きいほど、スコアと実際の購入(もしくはクリックなど)の結びつきが強いと評価できる。
新しい投稿
前の投稿
ホーム
登録:
投稿 (Atom)