- 投稿日:2019-12-19T23:32:12+09:00
学習記録 その7(11日目)
学習記録(10日目)
勉強開始:12/7(土)〜
使用書籍:大重美幸『詳細! Python3 入門ノート』(ソーテック社、2017年)【手書き文字の分類(Ch.16 / p.396)】 から再開(10日目)、
【ボストンの住宅価格分析(Ch.16 / p.408)】 まで終了(11日目)【読了】アヤメの分類
・hold out(学習データをある比率で分割し、訓練データとテストデータに分けること。)
ただし、データの並びが均一でない場合はよい学習成果が出ない。
その場合は shufflesplit を用いると、分割に合わせて要素のシャッフルが行われる。
・shufflesplitを用いて行う分析方法の1つに crossvalidation がある。
手元のデータ数が十分でない時に、学習に使用するデータを毎回分割、違う塊を選択し、繰り返す方法ボストンの住宅価格分析
・相関関係を予想する「回帰」処理を実施。
・書籍通りに進めていくと、どうもエラーが出る場所を発見。
モジュールがインポートされていないことによる関数の未定義だった。
最後のまとめにはちゃんと記載されていたが、途中の経過には記載がなかったため、何も考えずに進めていたら恐らく詰まる。
気づいただけでも、ちょっとした成長を実感。以上の内容をもって上記書籍は読了しました。
Progate講座
上記書籍を読了したものの、一番最初のこの段階でしっかりと基礎を固めておきたいと思い実施中です。
ガイドを受けつつ、1つのものを段階的に作りあげていくプログラムになっており、書籍使用による勉強とはまた違った側面でアウトプットを行えるので非常に勉強になっています。
Python学習コースは全部で5コースあり、今日一日で4まで終了しました。
Python以外にもJavaやRuby、Gitに関するコースまであったので、初心者には非常に有用だと感じました。
- 投稿日:2019-12-19T23:31:26+09:00
svmモジュールのSVCについて(数学は抜く。)
皆さんこんばんわ。
12/19の文を書いていきます。
今日は用事があって遅くなりましたね。。。今日は昨日勉強した直線SVCモデルの分類をまとめていこうと思います。
分類の目的
上の写真を見てください。
分類とはこの青の点とオレンジのバツを分ける線を自動的に見つけることです。
2次元だと人間にとってとても簡単ですよね?
SVCモデルは高次元の場合でも分ける為の式を求めれられるらしいので凄いですよね。
さぁ、これが何に役に立つの?と思った方も多いでしょう。
下の画像はいかがでしょうか?違いがわかりましたか?
そう、Gメールでは当たり前のように迷惑メールを弾いてくれますが、これはAIの力なのです。(アルゴリズムはもっと複雑です。)
けど、やっていることは同じで、ある直線(面)を引いてスパムか有効なメールかを分析しているのです。
美しいし、凄いですよね。さて、今日の最終の目的の画像を見せますね。
以下になります。⚠︎注意したいことがあります。
それはこの例の場合だと、無限の線が引けると言うことです。
よって、この点線の幅(マージ)が一番広くなった上で、その真ん中に実線(決定境界)を引きます。さてこの決定境界を求める為のコードを見ていきます。
サンプルコード
import numpy as np from sklearn.svm import SVC import matplotlib.pyplot as plt xmin = 0 xmax = 2 ymin = 0 ymax = 2 # 0 ~ 1 までの正規分布に従った値が入った(100行2列(X&Y))の行列を作成 X0 = np.random.uniform(size=(100, 2)) y0 = np.repeat(0, 100) X1 = np.random.uniform(low=1.0, high=2.0, size=(100, 2)) y1 = np.repeat(1, 100) svc = SVC(kernel='linear', C=1e8) # 学習 # svc.fit(train_features, train_labels) svc.fit(np.vstack((X0, X1)), np.hstack((y0, y1))) fig, ax = plt.subplots() # ax.scatter(x, y) 散布図 ax.scatter(X0[:, 0], X0[:, 1], marker='o', label='class 0') ax.scatter(X1[:, 0], X1[:, 1], marker='x', label='class 1') # 格子点を準備 xx, yy = np.meshgrid(np.linspace(xmin, xmax, 100), np.linspace(ymin, ymax, 100)) # decision_functionの為に一つの行列に。 xy = np.vstack([xx.ravel(), yy.ravel()]).T # 100 * 100 の行列。 # z = x*2 + y*2 とする部分のzの高さの部分。 # svcの決定関数を使ってxyという格子点からzを求めている。 z = svc.decision_function(xy).reshape((100, 100)) # 決定境界とマージンをプロット ax.contour(xx, yy, z, colors='k', levels=[-1, 0, 1], alpha=0.3, linestyles=['--', '-', '--']) plt.show()流れとしては、
1. データを準備
2. SVCのインスタンスを作成
3. 入力データをそれを紐付かせたラベルをfit関数に入れることで学習させる。
4. 格子点を作成(参考記事)
5. Zをfitで学んだdecision_functionを使って作成
6. contourを使って等高線を引くと、決定境界を書くことができる。大切な関数はfitとdecition_funkctionである。
fitの使い方は理解したが、decision_functionの方は分類器を利用してZを求めることくらいしか理解できてない。
それはもう少し勉強が進んで記事にしようと思う。今日は夜が遅いのでこれくらいで。
おやすみなさい。
- 投稿日:2019-12-19T23:31:15+09:00
この処理Perlでどう書く?
この処理Pythonでどう書く? の二番煎じです。
実際perlだったらどう書くかなと考えながら練習がてら。できる限りCOREモジュール縛り。
この処理Perlでどう書く?
標準出力・標準エラー出力
# 標準出力にメッセージ echo 'HELLO' # 標準エラーにメッセージ echo 'ERROR!' >&2use strict; use warnings; # 標準出力にメッセージ print "HELLO\n"; # もしくは v5.10 以上を use した上で say use v5.10; say "HELLO"; # 標準エラーにメッセージ。 print {*STDERR} "ERROR!\n"; # もしくは warn を使う。最後に改行がなければ警告の位置も教えてくれる。 warn "ERROR!\n";ファイル関係
パス操作
name="$(basename "/foo/bar/baz.txt")" # => baz.txt ファイル名 name="$(dirname "/foo/bar/baz.txt")" # => /foo/bar/ 親ディレクトリパス fullpath="/foo/bar/${name}" # => /foo/bar/baz.txt パスの連結use strict; use warnings; use File::Basename qw(basename dirname); use File::Spec; my $name = basename("/foo/bar/baz.txt"); # => baz.txt my $dir = dirname("/foo/bar/baz.txt"); # => /foo/bar $fullpath = File::Spec->catfile("/foo/bar", $name); # => /foo/bar/buz.txtチルダや環境変数が含まれるパスを扱う
import pathlib bad = pathlib.Path("~/Donwloads/report.txt") # ~ は自動展開されない! f = pathlib.Path("~/Donwloads/report.txt").expanduser() g = pathlib.Path("${WORK_DIR}/input.txt").expandvars()use strict; use warnings; my $bad = "~/Downloads/report.txt"; # globを使う。ワイルドカードに注意。 my $f = glob '~/Downloads/report.txt'; # スカラコンテキストでアクセスする。 # %ENV から環境変数を取れる。 my $g = "$ENV{WORK_DIR}/input.txt";ファイルの読み書き
import pathlib path = pathlib.Path('foo.txt') with open(path, 'r', encoding="utf-8") as f: for line in f: # line を処理 with open(path, 'w', encoding="utf-8") as f: print("内容", file=f) with path.open('r') as f: # Pathのメソッドを使って開くこともできる。 # ...use strict; use warnings; use open qw/:encoding(UTF-8) :std/; use utf8; use autodie; my $path = 'foo.txt'; READ: { open my $fh, '<', $path; while (<$fh>) { chomp(my $line = $_); # $line を処理 } close $fh; } WRITE: { open my $fh, '>', $path; print {$fh} "内容"; close $fh; } 1;行数を数える(
wc -l)with path.open('rb') as f: count = sum(1 for line in f)use strict; use warnings; use autodie; open my $fh, '<:raw', $path; my $count = () = <$fh>; # リストコンテキストでファイルハンドルを取り、それにスカラコンテキストでアクセス。ファイルの列挙
import pathlib dir = pathlib.Path('/tmp') for file in dir.glob('tmp.*'): # ファイルを処理する # fileは文字列ではなく、pathlib.Pathであることに注意use strict; use warnings; for my $file (glob '/tmp/tmp.*') { # ファイルを処理する # $fileは文字列 }ファイルの情報(存在確認・作成日時)
import pathlib f = pathlib.Path('/bin/bash') f.exists() # 存在確認 f.is_file() # ファイル? f.is_dir() # ディレクトリ? f.stat().st_ctime # 作成日時 f.stat().st_mtime # 更新日時 f.stat().st_atime # アクセス日時use strict; use warnings; use File::stat; my $f = '/bin/bash'; my $sb = stat($f); # 特殊ファイルハンドル "_" がキャッシュされる。 -e _; # 存在確認 -f _; # ファイル? -d _; # ディレクトリ? $sb->ctime; # 作成日時 $sb->mtime; # 更新日時 $sb->atime; # アクセス日時移動・削除
import pathlib path_from = pathlib.Path('.bash_history') path_to = pathlib.Path('/tmp/.bash_history') path_from.rename(path_to) # 移動 path_from.unlink() # 削除use strict; use warnings; use File::Copy qw(move); use autodie qw(move); my $path_from = '.bash_history'; my $path_to = '/tmp/.bash_history'; move $path_from, $path_to; # 移動 unlink $path_from; # 削除コピー
import shutil import pathlib path_from = pathlib.Path('.bash_history') path_to = pathlib.Path('/tmp/.bash_history') shutil.copy2(path_from, path_to) # コピーuse strict; use warnings; use File::Copy qw(copy); use autodie qw(copy); my $path_from = '.bash_history'; my $path_to = '/tmp/.bash_history'; copy $path_from, $path_to;外部コマンド
単純に実行する
import subprocess subprocess.run(['sl', '-h'], check=True)use strict; use warnings; use autodie qw(system); system qw(sl -h);外部のコマンドを実行し、標準出力を受け取る
import subprocess r = subprocess.run(['echo', '世界'], check=True, stdout=subprocess.PIPE) r.stdout # => b'\xe4\xb8\x96\xe7\x95\x8c\n' '世界\n'をUTF-8でエンコードしたもの r.stdout.decode(sys.getfilesystemencoding()) # => '世界\n'use strict; use warnings; use autodie; use Encode; # 実行するコマンドが安全なら my $stdout = `echo 世界`; $stdout; # '世界\n'のバイナリ文字列。utf8フラグは無し。 decode_utf8($stdout); # '世界\n'、utf8フラグあり。 # 安全ではないなら my @cmd = qw(echo 世界); open my $fh, '-|', @cmd; $stdout = do { local $/; <$fh> }; # 丸呑み close $fh; # utf8フラグの事を気にしたくないのなら { use utf8; use open qw/:encoding(UTF-8) :std/; open my $fh, '-|', @cmd; $stdout = do { local $/; <$fh> }; # utf8フラグあり close $fh; } # 古いシステムでロケールがUTF-8じゃない?正気か?? { no utf8; use Encode::Locale; # non-core my @lcmd = map { encode(locale => decode_utf8($_)) } @cmd; open my $fh, '-|', @lcmd; $stdout = decode(locale => do { local $/; <$fh> }); close $fh; }環境変数やカレントディレクトリを変更する
env = dict(os.environ) # Pythonスクリプトの環境変数をコピー env['DB_HOST'] = 'localhost' # 環境変数を変更 cwd = pathlib.Path('/') subprocess.run(['setup.sh'], cwd=cwd, env=env)use strict; use warnings; use Cwd qw(chdir); # chdir時に$ENV{PWD}をアップデートする use autodie; local $ENV{DB_HOST} = 'localhost'; my $orig_dir = Cwd::getcwd(); chdir "/"; system "setup.sh"; chdir $orig_dir;リダイレクトを使う
import subprocess import os.path fi = open(os.path.expanduser('~/.bash_history')) fo = open('p.txt', 'wb') subprocess.run(['grep', 'python[23]'], stdin=fi, stdout=fo) # p.txt に検索結果が出力される# COREモジュール縛り use strict; use warnings; use autodie; use IPC::Open2; open my $fh_history, '<', scalar glob '~/.bash_history'; my ($fh_out, $fh_in); my $pid = open2($fh_out, $fh_in, 'grep', 'python'); print {$fh_in} $_ while <$fh_history>; close $fh_in; close $fh_history; open my $fh_txt, '>:raw', 'p.txt'; print {$fh_txt} $_ while <$fh_out>; close $fh_txt; close $fh_out; waitpid $pid, 0; my $return_code = $? >> 8;# COREモジュール以外も使っていい場合 use strict; use warnings; use IPC::Run3 qw(run3); # non-core my $fi = scalar glob '~/.bash_history'; my $fo = 'p.txt'; run3 ['grep', 'python[23]'], $fi, $fo;パイプを使う
p1 = subprocess.Popen(["cat", '.bash_history'], stdout=subprocess.PIPE) p2 = subprocess.Popen(["grep", "python"], stdin=p1.stdout, stdout=subprocess.PIPE) p1.stdout.close() output = p2.communicate()[0] # history から 'python' を含む行を検索した結果 p2.returncode # grep の終了コードuse strict; use warnings; use autodie; use IPC::Open2; open my $fh_history, '<', scalar glob '~/.bash_history'; my ($fh_out, $fh_in); my $pid = open2($fh_out, $fh_in, 'grep', 'python'); print {$fh_in} $_ while <$fh_history>; close $fh_in; close $fh_history; waitpid $pid, 0; my $returncode = $? >> 8; my $output = do { local $/; <$fh_out> }; close $fh_out;spawn → wait (外部コマンドを起動し、終了を待つ)
import subprocess p1 = subprocess.Popen(['/path/to/heavy-sub-process1']) p2 = subprocess.Popen(['/path/to/heavy-sub-process2']) p1.wait() p2.wait()use strict; use warnings; my @cmds = ( ['/path/to/heavy-sub-process1'], ['/path/to/heavy-sub-process2'], ); my @pids; for my $cmd (@cmds) { my $pid = fork; if (!defined $pid) { die "Can't fork: $!"; } elsif (!$pid) { exec @{$cmd}; exit 1; } else { push @pids, $pid; } } my @rcs; for my $pid (@pids) { waitpid $pid, 0; push @rcs, $? >> 8; }シェルを実行する
subprocess.run('echo Hello > /dev/null', shell=True)system 'echo Hello > /dev/null'; # メタキャラクタがあればshellが呼ばれる時刻関係
from datetime import datetime, timedelta # datetimeは日時を、timedeltaは経過時間を表す epoch = datetime.now().timestamp() # => 1540277405.379158 現在のUnix時刻(小数) datetime.fromtimestamp(1540277405).strftime('%FT%T') # => '2018-10-23T15:50:05' start = datetime.now() # 何か時間がかかる処理をする duration = datetime.now() - start # datetime - datetime は timedelta print(duration / timedelta(seconds=1)) # 経過時間を数値型にするには、別の timedeltaで割る # => 42.680422 かかった秒数(小数) print(duration.total_seconds()) # これでもOK # => 42.680422 かかった秒数(小数)use strict; use warnings; use v5.12; use Time::Piece; use Time::HiRes; my $epoch = localtime->epoch; # => 現在のUnix時刻(整数) my $start = localtime; # なにか時間がかかる処理をする my $duration = localtime() - $start; # Time::Piece - Time::Piece は Time::Seconds say $duration->seconds; # => かかった秒数(整数) my $frac_epoch = Time::HiRes::time(); # => 現在のUnix時刻(小数) my $frac_duration = Time::HiRes::time() - $frac_epoch; say $frac_duration; # => かかった秒数(小数)文字列関係
文字列への式埋め込み
message='世界!' print(f"Hello {message}") # => Hello 世界!use strict; use warnings; my $message = '世界!'; print "Hello ${message}\n"; # => Hello 世界!print(f"1 + 2 = {1 + 2}") # => 1 + 2 = 3use strict; use warnings; # 文字列に埋め込めるようリファレンスをくぐらせる print "1 + 2 = ${\(1 + 2)}\n"; # => 1 + 2 = 3ヒアドキュメント
import textwrap report = textwrap.dedent(f""" レポート 日付: {date} """)use strict; use warnings; use v5.26; use Time::Piece; my $report = <<~EOF # 比較的新しいperlのdedent レポート 日付: ${\(localtime->strftime("%Y年%h月%d日"))} EOFコマンドライン引数
import sys sys.argv # => ['a.py', 'input.txt', '-o', 'output.txt']# プログラムファイル名自体は $0 に入っている。 @ARGV; # => ('input.txt', '-o', 'output.txt')import argparse parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('integers', metavar='N', type=int, nargs='+', help='an integer for the accumulator') parser.add_argument('--sum', dest='accumulate', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') args = parser.parse_args() print(args.accumulate(args.integers))#!/usr/bin/perl use v5.12; use strict; use warnings; use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat permute); use Pod::Usage qw(pod2usage); use File::Basename qw(basename); use List::Util qw(max sum); # 無理やりpythonのargparseに寄せたので多少ぎこちないかも。 my %args = ( accumulate => \&max, ); GetOptions( 'h|help' => sub { pod2usage(0) }, sum => sub { $args{accumulate} = \&sum }, ) or pod2usage(1); for my $arg (@ARGV) { if ($arg =~ /\D/) { pod2usage( -exitval => 1, -msg => "argument N: invalid int value: $arg", ); } } say $args{accumulate}->(@ARGV); 1; __END__ =pod =encoding UTF-8 =head1 NAME test.pl -- blah blah =head1 SYNOPSIS test.pl [-h] [--sum] N [N ...] Process some integers. =head2 OPTIONS =over 2 =item C<-h> show this help message =item C<--sum> sum the integers (default: find the max) =back =cut終了時の処理&シグナルをtrapする
import atexit import os tf = '/path/to/tempfile' @atexit.register def cleanup(): os.remove(tf)use strict; use warnings; use autodie; my $tf = '/path/to/tmp'; $SIG{HUP} = $SIG{INT} = $SIG{QUIT} = $SIG{TERM} = \&cleanup; sub cleanup { unlink $tf; }HTTPリクエスト(curlやwgetの代替)
# URLを、GETでリクエストし、レスポンスボディを出力する import urllib.request import urllib.parse params = urllib.parse.urlencode({'spam': 1, 'eggs': 2, 'bacon': 0}) url = f"http://www.musi-cal.com/cgi-bin/query?{params}" with urllib.request.urlopen(url) as f: print(f.read().decode('utf-8'))# URLを、GETでリクエストし、レスポンスボディを出力する use strict; use warnings; use v5.12; use HTTP::Tiny; my %params = (spam => 1, eggs => 2, bacon => 0); my $url = 'http://www.musi-cal.com/cgi-bin/query'; my $response = HTTP::Tiny->new->get($url, \%params); die "Failed" unless $response->{success}; say $response->{content};
- 投稿日:2019-12-19T23:27:51+09:00
NHKから国民を守る党から国民からNHKを守る党を守る党からNHKを守る党は矛盾しているか?
お前は何を言ってるんだ
先にこの記事をお読みください。
本記事では、元記事で問われていた「政党名を与えたとき、世界が矛盾しているか否かを判定するアルゴリズム」を Python で実装していきます。
数学的に定式化する
NHKを
0, 国民を1とすると、「NHKから国民を守る党」は「0から1を守る党」と表すことができます。それを更に省略してリスト
[0, 1]で表すことにすると、全政党が長さ2のリストで表現できます。例えば、「国民からNHKを守る党から国民を守る党」は
[[1, 0], 1]で表現され、
「NHKから国民を守る党から国民からNHKを守る党を守る党からNHKを守る党」は[[[0,1],[1,0]],0]と簡潔に表すことができます。そして考えうる全ての政党(矛盾するものも含め)がこのように表記できます。
友好性の公理
一般に、政党
[x, y]が実在するとき次が成り立っていることが期待されます。
- 政党
[x, y]はyと友好的である(なぜなら前者は後者を守っているから)。- 政党
[x, y]はxと友好的でない(なぜなら前者は後者から何かを守ろうとしているから)。また元記事では以下の公理も仮定しています。
xとyが友好的でありyとzが友好的であるならば、xとzも友好的である(味方の味方は味方)いま「
xとyが友好的である」ことをx ~ yと表記すると、
上の公理は「関係~は推移的である」と言い換えることができます。また、反射律(自分は自分と友好的)と対称律(
xとyが友好的ならばyとxも友好的)も成り立つと考えられるため、関係〜は同値関係となっています。よって、全ての政党は同値類で重複なく分割されることがわかります。
第3勢力は存在するか?
さて、全ての政党が何らかの陣営にきれいに分割されることがわかりましたが、
その陣営はいくつまで存在できるのでしょうか?
NHKにも国民にも与しない「第3の勢力」は果たして存在するのでしょうか?いま、公理1より任意の政党
[x,y]に対し[x,y] ~ yが成り立っていますが、
前者より後者の方が「表記に用いるカッコの数」が少ないことは明らかです。つまり任意の政党に対して、それよりカッコが少ない友好的な政党(またはNHK or 国民)を見つけることができます。
そして自分よりカッコが少ない友好的政党を見つける作業を続けていくと、
いずれはカッコが0のもの、つまり NHK か国民に行き着くことがわかります。よって、任意の政党
[x, y]は NHK か国民のいずれかと必ず友好的であり、
どちらかの同値類に含まれるため、第3の勢力などいないことがわかります。矛盾するマニフェスト
公理2により任意の政党
[x, y]において[x, y] !~ xが成り立ちますが、
上の議論により全ての政党はNHKか国民の陣営に属するため、これは[x, y]が属していない陣営にxが属していることを意味します。このとき
[x, y]が両方の陣営に属しているとすると、xはどちらの陣営にも属していないことになり矛盾します。よって、無矛盾な世界にNHKと国民両方に味方する政党は存在できず、
また何らかの政党が存在するならばNHKと国民は敵対していなければならないことが従います。逆(対偶)に言うとNHKと国民が友好的であるような世界では政党などいらないと言うことなのでしょう。
政党が矛盾するとき
xとyが同じ陣営だとすると、公理1より[x, y]はyの陣営(xの陣営)に属し、公理2によりxの陣営には属さないことになるので、矛盾します。これと同等の論理をプログラムで扱いやすくするために、
両陣営に属する政党がいても良いような世界を考えてみます。やさしい世界
一旦公理2を弱めた次の公理2'を考えてみます。
[x, y]は、xがNHK陣営なら国民陣営に属し、xが国民陣営ならNHK陣営に属する公理2が成り立っているときは(公理1のもとで)公理2’も成り立ちますが、
その逆は言えません。また公理2’の元では、NHKと国民の両方に味方する政党がいても矛盾しません。
なぜならある政党が何らかの陣営に属していることは言えても、何らかの陣営に属していないことを導く方法はなく、矛盾が起きえないからです。例えば「NHKからNHKを守る党」は公理1からNHK陣営に属しており、
公理2からNHK陣営に属してないことになり矛盾するわけですが、
これが公理2'であれば、NHKじゃない方の陣営、国民陣営にも属していることが言えるだけで、
何ら矛盾はおきません。みんななかよしと言うだけの話です。このやさしい世界の中において、両陣営となかよしな政党は元々の世界で考えると矛盾した政党であることは明らかだと思います。
よって、やさしい世界における陣営を調べれば、元々の世界の矛盾が判定できることになります。
陣営関数
やさしい世界において、任意の政党に対しそれが属する陣営を割り当てる関数
T(x): x \mapsto \{ \{0\}, \{1\}, \{0, 1\} \}が定義できます。
このとき、任意の政党
[x, y]に対して、T(x) \cap T(y) \neq \varnothing \Rightarrow T([x, y]) = \{0, 1\} \\ T(x) \cap T(y) = \varnothing \Rightarrow T([x, y]) = T(y)が成り立つことがわかります。
つまり政党
[x, y]が元々の世界で矛盾しているかどうかは、やさしい世界において陣営を再帰的に調べて行けば判定できることになります。Pythonで判定する
前節の内容をそのままプログラムにします。
def T(party): if party in [0, 1]: return [party] else: p = T(party[0]) q = T(party[1]) if len(set(p) & set(q)) != 0: return [0, 1] else: return qNHKから国民を守る党から国民からNHKを守る党を守る党からNHKを守る党は矛盾しているか?
print(T([[[0,1],[1,0]],0])) # => [0, 1]両陣営に属しているので矛盾していることがわかりました。
以上です。間違っていたら教えてください。
補遺A:矛盾しない政党はどれくらい存在するか?
いま政党のランク
R(x)を次のように定義します。R(0) = R(1) = 0, \\ R(x, y) = max(R(x), R(y)) + 1例えば、$$R([1, 1]) = max(0, 0) + 1 = 1$$となります。
ランク0はNHKと国民の2つのみで、どちらも矛盾はしていません(実情は別として)。
ランク1の政党は
[0, 0], [0, 1], [1, 0], [1, 1]の4つで、このうち矛盾していないのは2つです。ランク2の政党は、ランク1以下の6つの集団のそれぞれ組み合わせとして得られるので、
6 * 6 = 36個存在することがわかります。
また、ランク1以下の無矛盾な政党にはNHK陣営のものが2つ、国民陣営が2つあり、敵陣営との組み合わせの場合のみ矛盾しない政党ができあがるので、
ランク2の無矛盾な政党は4 * 2 = 8個存在することがわかります。同様に、ランク3の政党は
(2 + 4 + 36)^2 = 1764個存在し、そのうち無矛盾なのは12 * 6 = 72個しかないことがわかリます。この差はランクが増えるとどんどん開いていくので、世の中の政党のほぼ全てが矛盾していると言うことができます。
一般にランク
nの政党の数をp_n, 無矛盾な政党の数をc_nとおくとき、それらはp_0 = 2, \\ p_n = (\Sigma_{k=1}^{n-1} p_k)^2 \\と
c_0 = 2, \\ c_n = \frac{1}{2} (\Sigma_{k=1}^{n-1} c_k)^2の漸化式で求められます。
〔補遺終わり〕
- 投稿日:2019-12-19T23:18:46+09:00
pygameで風船割りゲーム
はじめに
前回の投稿では、tkinterを使ったターン制の三目並べを作成したので、今回はpygameを使った常に画面が更新されるものを作ってみました。
タイトルで風船割りゲームと書きましたがそこまでのゲーム性はありません。笑
ただ、今後ゲームを作る際に使えそう?な処理は学べたかなぁ。。。と思います。
例によって、欲しい機能を寄せ集めたキメラティックコードなので読みやすさは今後の課題ですね。
今回の完成品 一番最後に今回のコード全体像を載せてあります。
表示画面作成
まずは基盤となる画面作成からですね。
import pygame pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Game")なにはともあれpygameをimport。
pygame.init()で、pygameのモジュールを初期化してあげます。開幕に書く場合は、お決まりの呪文レベルの認識でいいかもしれません。大量のモジュールを使用する場合は、使ってないモジュールを初期化してあげるといいとかなんとか。そのうち処理の途中で書く日が来るかも?pygame.display.set_modeが画面サイズで横幅、縦幅。
set_captionはタイトルバーに表示される名前です。画面更新処理
import time FPSCLOCK = pygame.time.Clock() FPS = 15 class CanonGame(): def __init__(self): while True: pygame.display.update() FPSCLOCK.tick(FPS) def main(): CanonGame() if __name__ == '__main__': main()主役。
pygame.display.update()で画面の描画を反映した後に、pygame.time.Clock.tick(フレーム数)で1秒間に(フレーム数)を超えない速度で処理が実行されます。フレーム数を上げれば、滑らかな動きを実装することができる反面、処理が重くなってしまいます。
それと、基本的に更新し続けるものはwhile True:の範囲中に入れてあげてください。キーイベント取得
from pygame.locals import * pygame.key.set_repeat(5, 5) class CanonGame(): while True: for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() elif event.type == KEYDOWN: if event.key == K_SPACE: self.bullet_shot = True elif event.key == K_UP and self.cannon_radian < 89: self.cannon_radian += 1 elif event.key == K_DOWN and self.cannon_radian > 0: self.cannon_radian -= 1ゲームとなればやはり操作したいですよね?
event.get()で行われた操作の種類を取得します。
event.typeがQUIT(画面右上の×ボタン)ならすべてを終了させます。
KEYDOWN(キーボード押下)ならその後にevent.keyを取得して押されたキーごとに処理を割り振ります。
今回は、スペースキーで球を発射。上下キーで向き調節にしました。
key.set_repeatを設定することによりキーを押しっぱなしでも反応してくれるようになります。画像表示、回転
class CanonGame(): def __init__(self): bullet_image = pygame.image.load("bullet.png") self.cannon_image = pygame.image.load("cannon.png") screen.fill((255, 255, 255)) screen.blit(bullet_image, (self.bullet_x, self.bullet_y)) rotate_cannon = pygame.transform.rotate(self.cannon_image, self.cannon_radian - 45) cannon_rect = rotate_cannon.get_rect() cannon_rect.center = (84, 536) screen.blit(rotate_cannon, cannon_rect)pygame.image.load(”画像名”)で画像を読み込みます。画像の保存場所は実行ファイルを保存している場所と同じ場所で。
screen.fill(Red,Green,Blue)は背景色です。今回は背景画像を用意していないので、毎回背景を塗り替えて、直前の画像たちを消しています。これがないと前の処理たちがそのまま残ってしまいます。
screen.blit(画像,(x, y)):画像の配置で、気を付けないといけないのが、表示画面の左上がx:0、y:0になります。
画面の右下に向かうにつれてxとyの値が大きくなります。諸々の挙動
ゲーム内の挙動ですが、いくつかピックアップして解説します。
from random import randint self.balloon_x = randint(500, 700)randint(最小値, 最大値)。これにより風船が出現するx座標をランダムにしています。intなので設定値内の整数からランダムで選ばれます。
import sys sysfont = pygame.font.SysFont(None, 36) while True: score_image = sysfont.render("score : {}".format(self.score), True, (0, 0, 255)) screen.blit(score_image, (10, 20))文字の出力。
pygame.font.Sysfont(name, size, bold, italic)と4つのパラメータを変えることが出来ます。
sysfont.renderの{ }の中にはformat()が入ります。def set_balloon(self): now_time = time.time() if self.is_collision: if now_time - self.balloon_break > 1: self.is_collision = False if self.is_collision is False: if self.is_balloon: self.balloon_y = self.balloon_y - 12 self.balloon_alive = time.time() if self.balloon_alive - self.balloon_create > 4: self.is_balloon = False else: self.collision_check() else: self.balloon_image = pygame.image.load("balloon-red.png") self.balloon_x = randint(500, 700) self.balloon_y = 600 self.is_balloon = True self.balloon_create = time.time()あと、ごにょごにょ書いているset_balloon関数なんですが、ここではtimeモジュールを利用しています。
風船が出現したときに、balloon_createとして現在時刻を取得し、その後、balloon_aliveという時間を取得し続けて、差が4秒以上になれば再び風船を出現させるようにしています。
ついでにnow_timeというのも計測して、風船が割れたら1秒後に風船を出現させるようにしています。n秒後に処理させるって調べるとsleepが出てきますが、あれを使うと処理全体が止まるので注意してください。
import math def bullet(self): gravity = 9.8 if self.bullet_shot: bullet_speed_x = self.bullet_speed * math.cos(math.radians(self.cannon_radian)) bullet_speed_y = (self.bullet_speed * math.sin(math.radians(self.cannon_radian))) self.bullet_x = self.bullet_x + bullet_speed_x self.bullet_y = self.bullet_y - bullet_speed_y + gravity * self.time self.time += 0.2 if self.bullet_x > 800 or self.bullet_y > 600: self.bullet_shot = False self.set_bullet()発射した球の挙動は、発射角にサインコサインでそれっぽい動きになるように。
ちゃんとした物理式を使うか迷ったんですが、それっぽい動きになったからヨシっ!球が画面外に出るまで次の球は発射させません。
※この記述の場合、画面内に球がある状態で発射すると、飛行中の球が消えてしまいます。
なので、連射したい場合はそれぞれを別のものとして扱う必要があります。def collision_check(self): distance_y = ((self.balloon_y + 15) - (self.bullet_y + 16))**2 distance_x = ((self.balloon_x + 20) - (self.bullet_x + 16))**2 distance = (distance_x + distance_y)**(1/2) if distance < 31: self.pang_image = pygame.image.load("pang.png") self.pang_rect = self.pang_image.get_rect() self.pang_rect.center = (self.balloon_x + 20, self.balloon_y + 15) self.is_collision = True self.is_balloon = False self.total_score(100) self.balloon_break = time.time()衝突判定。
今回は球と風船のだいたいの中心からの絶対値で計算しています。久々すぎて三平方の定理ググりました。
ヒットボックスが円だと楽でいい!(四角の場合は、x軸同士とy軸同士の差を絶対値を出してandでうんぬんかんぬん)
衝突すると100点入ります。終わりに
色々な挙動を覚えるにはやはり触れてみるのが一番ですね。
今回の要素を組み合わせれば、簡単なゲームなら大体できそうな気がしなくもないです。
あと、イラスト描けるようになりたい・・・コード全体像
import math import sys import pygame from pygame.locals import * from random import randint import time pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Game") sysfont = pygame.font.SysFont(None, 36) pygame.key.set_repeat(5, 5) FPSCLOCK = pygame.time.Clock() FPS = 15 class CanonGame(): def __init__(self): bullet_image = pygame.image.load("bullet.png") self.set_cannon() self.set_bullet() self.bullet_shot = False self.is_balloon = False self.is_collision = False self.score = 0 while True: for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() elif event.type == KEYDOWN: if event.key == K_SPACE: self.bullet_shot = True elif event.key == K_UP and self.cannon_radian < 89: self.cannon_radian += 1 elif event.key == K_DOWN and self.cannon_radian > 0: self.cannon_radian -= 1 self.set_balloon() self.bullet() screen.fill((255, 255, 255)) screen.blit(bullet_image, (self.bullet_x, self.bullet_y)) if not self.is_collision: screen.blit(self.balloon_image, (self.balloon_x, self.balloon_y)) elif self.is_collision: screen.blit(self.pang_image, self.pang_rect) rotate_cannon = pygame.transform.rotate(self.cannon_image, self.cannon_radian - 45) cannon_rect = rotate_cannon.get_rect() cannon_rect.center = (84, 536) screen.blit(rotate_cannon, cannon_rect) score_image = sysfont.render("score : {}".format(self.score), True, (0, 0, 255)) screen.blit(score_image, (10, 20)) pygame.display.update() FPSCLOCK.tick(FPS) def set_bullet(self): self.bullet_x = 68 self.bullet_y = 520 self.bullet_speed = 50 self.time = 0 def set_cannon(self): self.cannon_image = pygame.image.load("cannon.png") self.cannon_radian = 45 def set_balloon(self): now_time = time.time() if self.is_collision: if now_time - self.balloon_break > 1: self.is_collision = False if self.is_collision is False: if self.is_balloon: self.balloon_y = self.balloon_y - 12 self.balloon_alive = time.time() if self.balloon_alive - self.balloon_create > 4: self.is_balloon = False else: self.collision_check() else: self.balloon_image = pygame.image.load("balloon-red.png") self.balloon_x = randint(500, 700) self.balloon_y = 600 self.is_balloon = True self.balloon_create = time.time() def bullet(self): gravity = 9.8 if self.bullet_shot: bullet_speed_x = self.bullet_speed * math.cos(math.radians(self.cannon_radian)) bullet_speed_y = (self.bullet_speed * math.sin(math.radians(self.cannon_radian))) self.bullet_x = self.bullet_x + bullet_speed_x self.bullet_y = self.bullet_y - bullet_speed_y + gravity * self.time self.time += 0.2 if self.bullet_x > 800 or self.bullet_y > 600: self.bullet_shot = False self.set_bullet() def collision_check(self): distance_y = ((self.balloon_y + 15) - (self.bullet_y + 16))**2 distance_x = ((self.balloon_x + 20) - (self.bullet_x + 16))**2 distance = (distance_x + distance_y)**(1/2) if distance < 31: self.pang_image = pygame.image.load("pang.png") self.pang_rect = self.pang_image.get_rect() self.pang_rect.center = (self.balloon_x + 20, self.balloon_y + 15) self.is_collision = True self.is_balloon = False self.total_score(100) self.balloon_break = time.time() def total_score(self, score): self.score = self.score + score def main(): CanonGame() if __name__ == '__main__': main()
- 投稿日:2019-12-19T23:17:00+09:00
Alpine Linux 3.10.3 Python3 NLTKを実行する
実行環境
docker-compose 1.23.2
VirtualBox 5.2.20
NLTK 3.4.5ディレクトリ構造
/ ├docker/ └ Dockerfile ├src/ └ test.py └docker-compose-ymlDcokerfile
FROM python:3.7.5-alpine # https://www.nltk.org/data.html # > The downloader will search for an existing nltk_data directory to install NLTK data. RUN mkdir /usr/share/nltk_data RUN pip3 install nltk && python3 -m nltk.downloader all
python3 -m nltk.downloader allを実行してダウンロードするライブラリを/usr/share/nltk_dataに保存します。
(ディレクトリを指定する場合はnltk.downloader -d)docker-compose.yml
version: '3' services: nltk: build: context: ./docker image: image-nltk container_name: container-nltk volumes: - ./src:/src tty: truetest.py
import nltk sentence = "NLTK is a leading platform for building Python programs to work with human language data." tokens = nltk.word_tokenize(sentence) print(tokens) tagged = nltk.pos_tag(tokens) print(tagged)ビルド
docker-compose buildコンテナ作成
docker-compose up -dスクリプト実行
dcoker-compose exec nltk ash以下コンテナ内部
/ # python3 /src/test.py ['NLTK', 'is', 'a', 'leading', 'platform', 'for', 'building', 'Python', 'programs', 'to', 'work', 'with', 'human', 'language', 'data', '.'] [('NLTK', 'NNP'), ('is', 'VBZ'), ('a', 'DT'), ('leading', 'VBG'), ('platform', 'NN'), ('for', 'IN'), ('building', 'VBG'), ('Python', 'NNP'), ('programs', 'NNS'), ('to', 'TO'), ('work', 'VB'), ('with', 'IN'), ('human', 'JJ'), ('language', 'NN'), ('data', 'NNS'), ('.', '.')]
- 投稿日:2019-12-19T22:14:58+09:00
光彦「ボクなら自己紹介で犯人わかりますよ!」
プロローグ
みなさんこんにちは。
今日は、ボクが考えた事件解決の助けとなる画期的な発明を紹介したいと思います。
それは…事件が起きる前の容疑者たちの会話を集めて、犯人と被害者を予測するシステムです!
AIについてはまだよくわからないけど、少年探偵団に不可能はありません!目的
- 事件が起きる前に犯人と被害者を予測する
条件
- 対象とする事件は殺人事件
- ストーリー開始から事件が起きる前までの会話から犯人と被害者を予測する
システム概要
- 言語はPython
- 機械学習に使うライブラリはkeras
- 形態素解析エンジンはjanome
実装の流れ
システム作成の手順を簡単に説明します。
データ収集
コナンの単行本からひたすらデータを収集します。
事件が起きるまでの会話を全部拾うのはきつかったので、吹き出し10個分を学習データとして利用することにしました。
また、会話に登場する人物名が判定に影響を与えないように、全ての人物名を「田中」に変換しました。形態素解析
収集したデータは日本語の文章となりますが、このままでは学習データとして使えないためデータ処理を行っていきます。
文章を単語区切りにすることで、このあと登場するベクトル化を可能にします。
例えば「すもももももももものうち」を「すもも/も/もも/も/もも/の/うち」のように区切ります。ベクトル化
janomeのdoc2vecを利用して文章をベクトルデータに変換します。
文章データを数値で表現することで、学習データとして利用できるようになりました。学習
出来上がったベクトルデータに「犯人/被害者/一般人」のラベル付けを行い、学習を行います。結果
学習データに対しては100%近い精度となりましたが、新規データへの精度はほとんど出ませんでした。
- 敗因:学習データが圧倒的に足りなかったです(28個)
- 単行本の内容を手打ちでデータ化するのは思ったより大変な作業でした
- 投稿日がせまってたので妥協しました
- 漫画読むの優先した
よく言われますが、機械学習で一番大変なのは学習データを集めることです。
こんなことがしたい!と思ったときにすぐにデータが手に入ることは稀だと思うので、普段からデータにアンテナを張っておくことが大切だと思いました。おまけ
いろんなキャラクターのセリフを判定してみた結果です。
左から順に[一般人 被害者 犯人]の確率です。
- 吾輩は猫である
- [0.506109 0.47779834 0.01609264]
- オッス!オラ悟空
- [0.7559725 0.22846058 0.01556693]
- 僕ドラえもん
- [0.6403506 0.3441662 0.01548317]
- 東中出身、涼宮ハルヒ。ただの人間には興味ありません。この中に宇宙人、未来人、異世界人、超能力者がいたら、あたしのところに来なさい。以上。
- [0.2473547 0.69323796 0.05940729]
- 僕は新世界の神となる!
- [0.28107646 0.70111287 0.01781074]
- あきらめたらそこで試合終了ですよ
- [0.63113874 0.34889922 0.01996207]
- 何の成果も!!得られませんでした!!
- [0.46731383 0.48855832 0.04412784]
- たった一つの真実見抜く、見た目は子ども、頭脳は大人、その名は名探偵コナン!
- [0.50221133 0.47582945 0.02195918]
ほとんど一般人か被害者の確率にかたよってしまいました。
犯人の確率は高くても6%と低いですね、もしかしたら派手なセリフは犯人ではないと学習しているのかもしれません。被害者の確率が最も高かったのはキラの「僕は新世界の神となる!」でしたね。
結論
夜神ライトは死ぬ
- 投稿日:2019-12-19T21:53:03+09:00
QoreSDKを使ってBTCの価格上昇・下降を予測してみる
だいぶ投稿が遅くなってしまいましたがこちらの記事は深層学習以外の機械学習と応用技術 by QuantumCore Advent Calendar 2019の7日目の記事です。
はじめに
まずはじめにこちらのQoreSDKの紹介記事読んで、Qoreの強みが時系列データに強く、学習が高速でデータが少なくても精度が出せるという点であるということを理解しました。
そこで通貨取引の価格上昇下降のタイミングが素早く検出できれば取引の利益を大きく損失を小さくすることができるのではないかと考えました。
次に扱う題材ですがBTC(ビットコン)に決めました。また、BTCの取引履歴はオープンデータとして公開されているため開発者が扱いやすいという利点も選択理由の一つです。というわけでKaggleに公開されていますこちらのデータをKaggleDataset - bitcoin-historical-dataを今回は使います。
これ以降の紹介で使っておりますソースコードは下記GooglaColabratoryで動作確認したコードになります。
GoogleColabratory - btc.ipynbデータの読み込み
まずKaggleDatasetからデータをダウンロードしたことを前提に話を進めます。GoogleColabratory上でKaggleDatasetからデータをダウンロードする方法については下記記事を参考にしました。
Qiita - Google Colab上でKaggleのデータをロード、モデル訓練、提出の全てを行う
まずはデータを読み込む部分です、元々加工済みのCSVファイルということもありpandasでそのまま読み込めます。
load_data.pyimport numpy as np import pandas as pd btc = pd.read_csv("/content/bitstampUSD_1-min_data_2012-01-01_to_2019-08-12.csv") btc.head() """ Timestamp Open High Low Close Volume_(BTC) Volume_(Currency) Weighted_Price 0 1325317920 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 1 1325317980 NaN NaN NaN NaN NaN NaN NaN 2 1325318040 NaN NaN NaN NaN NaN NaN NaN 3 1325318100 NaN NaN NaN NaN NaN NaN NaN 4 1325318160 NaN NaN NaN NaN NaN NaN NaN """データの前処理
データを見てみると欠損値が存在するため、直前の値で欠損値を埋めるffillを使って欠損値を埋めます。
また今回の予測対象である価格上昇・下降の定義ですが、終値(カラムではClose)が始値(カラムではOpen)よりも値が上回っていた場合は価格が上昇したと定義します。
最後にTimestampカラムをto_datetimeを使って扱いやすいように型変換しておきます。
preprocess.pybtc[btc.columns.values] = btc[btc.columns.values].ffill() btc['Delta'] = btc['Close'] - btc['Open'] def digitize(n): if n > 0: return 1 return 0 btc['label'] = btc['Delta'].apply(lambda d: digitize(d)) # https://stackoverflow.com/questions/19231871/convert-unix-time-to-readable-date-in-pandas-dataframe btc["Timestamp"] = pd.to_datetime(btc["Timestamp"], unit='s') btc.head() """ Timestamp Open High Low Close Volume_(BTC) Volume_(Currency) Weighted_Price Delta label 0 2011-12-31 07:52:00 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 0.0 0 1 2011-12-31 07:53:00 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 0.0 0 2 2011-12-31 07:54:00 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 0.0 0 3 2011-12-31 07:55:00 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 0.0 0 4 2011-12-31 07:56:00 4.39 4.39 4.39 4.39 0.455581 2.0 4.39 0.0 0 """ btc.tail() """ Timestamp Open High Low Close Volume_(BTC) Volume_(Currency) Weighted_Price Delta label 3997692 2019-08-11 23:56:00 11555.57 11555.57 11540.37 11540.58 0.036868 425.909106 11552.336234 -14.99 0 3997693 2019-08-11 23:57:00 11553.49 11556.22 11553.49 11556.22 0.623462 7204.428272 11555.520505 2.73 1 3997694 2019-08-11 23:58:00 11559.73 11561.22 11546.77 11561.22 0.159070 1838.731403 11559.252199 1.49 1 3997695 2019-08-11 23:59:00 11559.73 11589.73 11528.73 11528.73 16.198210 187504.635170 11575.638889 -31.00 0 3997696 2019-08-12 00:00:00 11527.44 11551.57 11520.00 11520.00 23.805939 274731.256920 11540.450291 -7.44 0 """モデルの学習に使う訓練データと評価に使うテストデータを作っていきます。
データを作る際の注意点としてQoreSDKの使い方について事前にメールにて以下のように注意書きがあったためそれに従うため、訓練・テストデータ共に時間長を約12時間(1サンプル1分間隔)に抑えています。
なお、一度にAPIに投げられるデータサイズに制限がありまして、N*T*V < 150,000 && N*T < 10,000の大きさまでのデータとして頂きますようお願いいたします。
またAPIのドキュメントを読むと訓練、予測時共に型が
numpy.ndarrayでありかつ学習・推論用データの次元が(データ数, 時間, 実データ)、正解ラベルの次元が(データ数, 1)であったのでそのように変換しておきます。
qcore-info - qore_sdk.client.WebQoreClient.classifier_train
X (numpy.ndarray) – 学習データ X.shape = (データ数, 時間, 実データ)
Y (numpy.ndarray) – 正解ラベル Y.shape = (データ数, 1)qcore-info - qore_sdk.client.WebQoreClient.classifier_predict
X (numpy.ndarray) – 推論用データ X.shape = (データ数, 時間, 実データ)
Y (numpy.ndarray) – 正解ラベル Y.shape = (データ数, 1)preprocess2.pydata = btc[('2019-01-01' < btc['Timestamp']) & (btc['Timestamp'] < '2019-01-01 12:00')] data """ Timestamp Open High Low Close Volume_(BTC) Volume_(Currency) Weighted_Price Delta label 3676577 2019-01-01 00:01:00 3694.72 3694.72 3690.65 3690.65 9.500151 35080.265871 3692.600865 -4.07 0 3676578 2019-01-01 00:02:00 3689.73 3689.73 3686.62 3686.62 0.965966 3562.371230 3687.884698 -3.11 0 3676579 2019-01-01 00:03:00 3692.85 3692.85 3688.32 3692.35 0.296662 1095.220713 3691.813285 -0.50 0 3676580 2019-01-01 00:04:00 3692.35 3692.35 3690.34 3690.34 0.111622 412.065433 3691.614849 -2.01 0 3676581 2019-01-01 00:05:00 3690.40 3690.85 3690.40 3690.85 2.247676 8295.406915 3690.659723 0.45 1 ... ... ... ... ... ... ... ... ... ... ... 3677291 2019-01-01 11:55:00 3706.01 3706.01 3706.01 3706.01 0.412970 1530.471061 3706.010000 0.00 0 3677292 2019-01-01 11:56:00 3706.01 3706.01 3706.01 3706.01 0.412970 1530.471061 3706.010000 0.00 0 3677293 2019-01-01 11:57:00 3706.01 3706.01 3706.01 3706.01 0.412970 1530.471061 3706.010000 0.00 0 3677294 2019-01-01 11:58:00 3706.01 3706.01 3706.01 3706.01 0.071164 263.734496 3706.010000 0.00 0 3677295 2019-01-01 11:59:00 3706.01 3706.01 3700.00 3700.00 14.862874 55009.746820 3701.151476 -6.01 0 """ labels = data["label"] data.drop(columns=["Timestamp", "label", "Delta"], inplace=True) data = np.array(data) labels = np.array(labels) data = data.reshape((data.shape[0], data.shape[1], 1)) labels = labels.reshape((labels.shape[0], 1)) test_data = btc[('2019-01-01 12:00' < btc['Timestamp']) & (btc['Timestamp'] < '2019-01-01 23:59')] test_data """ Timestamp Open High Low Close Volume_(BTC) Volume_(Currency) Weighted_Price Delta label 3677297 2019-01-01 12:01:00 3694.63 3694.63 3694.63 3694.63 2.572921 9505.991188 3694.630000 0.00 0 3677298 2019-01-01 12:02:00 3694.63 3694.63 3694.63 3694.63 2.572921 9505.991188 3694.630000 0.00 0 3677299 2019-01-01 12:03:00 3694.63 3694.63 3694.63 3694.63 2.572921 9505.991188 3694.630000 0.00 0 3677300 2019-01-01 12:04:00 3701.03 3704.20 3701.03 3704.20 2.176152 8060.808805 3704.156299 3.17 1 3677301 2019-01-01 12:05:00 3704.20 3704.20 3704.20 3704.20 0.045025 166.779901 3704.200000 0.00 0 ... ... ... ... ... ... ... ... ... ... ... 3678010 2019-01-01 23:54:00 3812.78 3821.38 3812.78 3821.38 21.387205 81679.855061 3819.099098 8.60 1 3678011 2019-01-01 23:55:00 3819.70 3819.70 3816.41 3816.41 1.699109 6488.410322 3818.713657 -3.29 0 3678012 2019-01-01 23:56:00 3816.41 3816.99 3813.81 3816.99 3.007816 11480.373641 3816.847055 0.58 1 3678013 2019-01-01 23:57:00 3816.41 3816.99 3813.81 3816.99 3.007816 11480.373641 3816.847055 0.58 1 3678014 2019-01-01 23:58:00 3816.41 3816.99 3813.81 3816.99 3.007816 11480.373641 3816.847055 0.58 1 """ test_labels = test_data["label"] test_data.drop(columns=["Timestamp", "label", "Delta"], inplace=True) test_data = np.array(test_data) test_labels = np.array(test_labels) test_data = test_data.reshape((test_data.shape[0], test_data.shape[1], 1)) test_labels = test_labels.reshape((test_labels.shape[0], 1))モデルの学習
準備した訓練データを使ってモデルの学習をしてゆきます。今回は分類問題なので評価結果をsklearn.metrics.classification_reportを使って求めます。
今回予測したかった価格の上昇がうまく学習できていないのかまったく予測できていません。train.pyfrom qore_sdk.client import WebQoreClient # api_keyの読み取り with open(api_key_path, "r") as f: api_key = json.load(f) client = WebQoreClient(**api_key) client.classifier_train(data, labels) # {'res': 'ok', 'train_time': 0.8283402919769287} response = client.classifier_predict(data) from sklearn.metrics import classification_report report = classification_report(labels, response['Y']) print(report) """ precision recall f1-score support 0 0.66 1.00 0.79 473 1 0.00 0.00 0.00 246 accuracy 0.66 719 macro avg 0.33 0.50 0.40 719 weighted avg 0.43 0.66 0.52 719 """モデルのテスト
最後に訓練データにはない未知データを使って予測してゆきます。
不思議なことに学習では一切予測が当たらなかった価格の上昇をそれなりに当てています。predict.pyresponse = client.classifier_predict(test_data) report = classification_report(test_labels, response['Y']) print(report) """ precision recall f1-score support 0 0.11 0.02 0.03 441 1 0.34 0.79 0.47 277 accuracy 0.32 718 macro avg 0.22 0.41 0.25 718 weighted avg 0.20 0.32 0.20 718 """参考
- 投稿日:2019-12-19T21:52:07+09:00
続 Google Could PlatformのOCRサービスを使ってサイマの課題に挑戦する
Ateam cyma Advent Calendar 2019、21日目です!
株式会社エイチーム EC事業本部サイマのエンジニア @shimura_atsushi が2回目の登場です。1回目でGoogle Could PlatformのOCRサービスを使ってサイマの課題に挑戦するとしてサイマの抱える課題に立ち向かう1手目の挑戦をしました。
2回目である今回は更に納品書チェックに対して踏み込んだ取り組みをしていきます。前回をおさらい
前回の投稿でGCPのサービスを利用して単純なOCRを試しました。しかし、今サイマで運用されている納品書類は文面が複雑なものが多くただOCRにかけただけでは文字おこしの精度は低く例えうまく文字おこしされたとしてもデータのラベリングがされておらず文字データの再利用性が乏しい状態でした。
今回は・・・
前回の反省を基に今回は「OCRをしやすい画像が用意する」にフォーカスしてOCRにかける画像の前処理を作成していきます。
内容は前回からの継続ですのでタイトルはそのままですが今回はPythonでの実装がメインでGoogle Could Platformに関しては薄めですあしからずご容赦ください。今回はサイマのエンジニアである@NamedPythonさんの勧めもあって画像処理ライブラリが充実している
Pythonを利用して行きます。環境を準備する
今回用いた開発端末
- 開発端末 MacBook Pro 15-inch
- OS macOS Mojave
もろもろインストールする
ここらへんはサクッと行きます。
インストール
Pythonをインストールする
pyenv
- pythonのインストールするバージョンを管理できる
python 3.8.0
- 執筆時点の最新を利用
pip
- pythonにおけるパッケージ管理ツール
- pythonをインストールすると一緒に入ると思います
pdf2imageインストール
- PDFからPNGやJPEGへ変換するのに利用
popplerインストール
- pdf2imageでPDF変換に利用
pillowインストール
- 画像の加工に利用、主に切り抜きに利用
opencvインストール
- 画像の加工に利用、主に2値化に利用
brew install pyenv pyenv install --list #インストール可能なバージョンを確認 pyenv install 3.8.0 pip3 install pdf2image brew install poppler pip install pillow pip install opencv大まかな流れ
- 書類を複合機にてスキャン
- スキャンデータ(PDF)を画像データに変換する
- 変換した画像データを切り抜く
- 切り抜いたデータの2値化する
- OCRにかける
複合機にてスキャン(PDF)
本社にある複合機を利用します、スキャンすると登録されているメールアドレスにPDFが添付されてきます。
※最終的な運用ではサイマの各工場でスキャンをする予定PDFから画像データへ変換する
スキャンされたデータはPDF形式なので画像データに変換します。
ディレクトリを指定すると格納されたPDFファイルを画像データに変換してくれるというものです。
pdf2imageをインポートしてconvert_from_pathというメソッドに変換したいファイルパスを渡せば
変換してくれます、簡単ですね。pdf2png.pyfrom pdf2image import convert_from_path from pathlib import Path import os p = Path('./img/pdf') pdf_list = os.listdir(p) print(pdf_list) for i, pdf_file_path in enumerate(pdf_list): images = convert_from_path('./img/pdf/{}'.format(pdf_file_path)) for image in images: image.save('./img/png/{}.png'.format(i), 'png')変換したデータを切り抜く
今回のOCRの肝はこの工程です。
前回の反省を基に複雑な納品書のデータから必要な箇所を切り取りラベリングするという処理を本工程で実装します。設定ファイルをJSONで用意する
納品書のフォーマットは仕入先ごとに基本的には一定(自転車とパーツで別パターンはある)なので切り抜くために必要な座標をJSON形式の設定ファイルを納品書フォーマットごとに用意します。
納品書上の内容で必要な情報は
- 仕入先名
- 納品日
- 商品番号
- 個数
- 単価
ですのでこれらが記載される箇所の座標を設定ファイルに持っておきます。
各仕入先ごとのJSON設定ファイル
shiiresaki_setting.json{ "wholesaler_id": 2, "warehouse": { "x":10, "y":10, "height":50, "width":100 }, "date": { "x":20, "y":20, "height":50, "width":100 }, "product": { "x":30, "y":30, "height":150, "width":200 }, "figure": { "x":40, "y":40, "height":200, "width":250 }, "price": { "x":50, "y":50, "height":200, "width":250 } }※数字は仮のものを記載してます。
pillowを用いた画像切り抜き処理
crop4image.pyfrom PIL import Image import sys import json import productsetting args = sys.argv p = productsetting.product.ProductSetting(args[1]) image = Image.open('img/png/{wholesaler_id}.png'.format(wholesaler_id=p.wholesaler_id)) rect = ( p.warehouse['x'], p.warehouse['y'], p.warehouse['x'] + p.warehouse['width'], p.warehouse['y'] + p.warehouse['height'] ) print(rect) cropped_image = image.crop(rect) cropped_image.save('{wholesaler_id}.png'.format(wholesaler_id=p.wholesaler_id))JSONによる設定ファイルを読み込むクラス
productsetting.pyimport sys import json class ProductSetting: CONFIG_SETTING_FILE_BASE_FORMAT = './settings/product/{wholesaler_id}.json' def __init__(self, wholesaler): config_file_path = open(self.CONFIG_SETTING_FILE_BASE_FORMAT.format(wholesaler_id=wholesaler), 'r') config = json.load(config_file_path) self.wholesaler_id = config['wholesaler_id'] self.warehouse = { 'x': config['warehouse']['x'], 'y': config['warehouse']['y'], 'height': config['warehouse']['height'], 'width': config['warehouse']['width'] } self.product = { 'x': config['product']['x'], 'y': config['product']['y'], 'height': config['product']['height'], 'width': config['product']['width'] } self.date = { 'x': config['date']['x'], 'y': config['date']['y'], 'height': config['date']['height'], 'width': config['date']['width'] } self.figure = { 'x': config['figure']['x'], 'y': config['figure']['y'], 'height': config['figure']['height'], 'width': config['figure']['width'] }このスクリプトを実行するとこのような画像から
※画像は加工してあります。切り抜いたデータを2値化する
次は切り取った画像のOCR精度を高めるために文字を2値化して読み取り精度の向上を図ります。
opencvを利用して作成
2値化のプログラムはシンプルにこんな感じでdeeply_character.pyimport cv2 img = cv2.imread('./result/png/1013/buyoption_1013.png', 0) threshold = 100 #しきい値 ret, img_thresh = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY) cv2.imwrite('./result/deeply/test/buyoption_1013.png', img_thresh)サンプルが良くないのかあまり恩恵を感じられないです。
なんと!よりはっきりとした画像になりました。
この画像を前回作成したGCPのOCRにかけてみます。
その結果・・・
このように文字起こしされました、よく考えると「配達日」という箇所もノイズになるので省いても良かったですね。
ただこの精度ならチェックに関する再利用性が保てそうです。まとめ
今回はOCRに掛ける前処理として、
- 必要な箇所だけの切り抜き
- 切り抜いた画像の2値化による鮮明化
をすることで如何にOCRに対して有利な状況を作れるかに挑戦してみました。
今回の事務処理効率化の取り組みですが事業部内で「やってみます!」と豪語したは良かったですが、納品書の現物を見ると本当に自動化できるのかと不安でした。
結果、画像の切り抜きによるノイズの除去と2値化による鮮明化の後でOCRにかけることで、精度が高めることでき、自動化が現実的になったかなと実感してます。アドベントカレンダー2回にわたりサイマにおける納品書チェックという課題にOCRを中心とした技術で自動化へ挑戦をはじめました。今後はシステムの実装を進めつつ工場を巻き込んた運用の実現に取り組みたいです。
最後に
Ateam cyma Advent Calendar 2019の21日目いかがでしたか。
22日目はサイマのデザイナである@ryo_cyさんがBEMを用いたCSS設計について話をしてくれますのでお楽しみに!株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。
エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。
- 投稿日:2019-12-19T21:37:51+09:00
pythonっぽいコードを書く
YoutubeでRaymond Hettingerさん(python core developper)がpythonでよくあるミスと正しい書き方を解説する動画を見つけたので、まとめます。
動画では、主にpython2を例に挙げてますが(2013年の動画なので)、ここではできるだけpython3に変換しました。今ではすでに古い書き方になっている場合があるので適宜調べながら使ってください。Loop
- できるだけiteratorを使う
シンプルなloop
だめな例for i in [0, 1, 2, 3, 4, 5]: print(i**2)リスト全体をメモリにおいてしまう。
↓良い例for i in range(6): print(i**2)
rangeがiteratorとして一つずつ生成するので、メモリを無駄に使わない。python2では
rangeはリスト、xrangeがitertor
python3ではrangeがiterator(python2のxrangeの名前が変わった)リストのloop
だめな例colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print(colors[i])↓
良い例colors = ['red', 'green', 'blue', 'yellow'] for color in colors: print(color)下の書き方のほうが速い
逆向きのloop
だめな例colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)-1, -1, -1): print(colors[i])↓
良い例colors = ['red', 'green', 'blue', 'yellow'] for color in reversed(colors): print(color)インデックスも得たい
だめな例colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print(i, '--->', colors[i])↓
良い例colors = ['red', 'green', 'blue', 'yellow'] for i, color in enumerate(colors): print(i, '--->', color)2つのリストを同時にloop
だめな例names = ['raymond', 'rachel', 'matthew'] colors = ['red', 'green', 'blue', 'yellow'] n = min(len(names), len(colors)) for i in range(n): print(names[i], '--->'. colors[i]↓
良い例names = ['raymond', 'rachel', 'matthew'] colors = ['red', 'green', 'blue', 'yellow'] for name, color in zip(names, colors): print(name, '--->', color)長さの違うiteratorがzipに入力されると、短い方に揃う。
長い方に揃えるためにはitertools.zip_longestpython2では
zipはリストを生成(izipがiterator)、
python3ではzipがiteratorを生成カスタムソート
だめな例colors = ['red', 'green', 'blue', 'yellow'] def compare_length(c1, c2): if len(c1) < len(c2): return -1 elif len(c1) > len(c2): return 1 else: return 0 print(sorted(colors, cmp=compare_length)↓
良い例colors = ['red', 'green', 'blue', 'yellow'] print(sorted(colors, key=len))keyによるソートで十分か?
十分でない例もあるがほとんどの場合は大丈夫。(SQLはソートを大量にやるが、keyによるソートでやっている)sentinel valueでループを止める
だめな例blocks = [] while True: block = f.read(32) if block == '': break blocks.append(block)↓
良い例blocks = [] for block in iter(functool.partial(f.read, 32), ''): blocks.append(block)partialが気持ち悪いが、iteratorとして扱えることのメリットが大きい
sentinel value(終了を示す値)はさけたほうが良い条件によってループから抜ける
だめな例def find(seq, target): found = False for i, value in enumerate(seq): if value == target: found = True break if not found: return -1 return iフラッグ(
found)を使わなきゃいけない場合の例
↓良い例def find(seq, target): for i, value in enumerate(seq): if value == target: break else: return -1 return ifor内でbreakがなかったらelse文が実行される。elseではなく
nobreakという名前にすべきだったと後悔しているそう。Dictionary
Dictionaryのキーをループ
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k in d: print(k)↑辞書に変更を加えるとおかしなことが起こる
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k in d.keys(): if k.startswith('r'): del d[k]↑
d.keys()が事前にリストのコピーを作るので、辞書の変更が可能キーと値をループ
だめな例d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k in d: print(k, '--->', d[k])↓
良い例d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k, v in d.items(): print(k, '--->', v)リストから辞書を作成
names = ['raymond', 'rachel', 'matthew'] colors = ['red', 'green', 'blue'] d = dict(zip(names, colors))リスト内の出現回数を数える
非効率な方法colors = ['red', 'green', 'red', 'blue', 'green', 'red'] d = {} for color in colors: if color in d: d[color] = 0 d[color] += 1↓
正しいやり方d = {} for color in colors: d[color] = d.get(color, 0) + 1↓
最近の方法d = defaultdict(int) for color in colors: d[color] += 1リストをグルーピングする
だめな例names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie'] d = {} for name in names: key = len(name) if key not in d: d[key] = [] d[key].append(name)↓
正しいやり方d = {} for name in names: key = len(name) d.setdefault(key, []).append(name)↓
最近の方法d = defaultdict(list) for name in names: key = len(name) d[key].append(name)
getは辞書に代入はしない。setdefaultは代入する複数の辞書をくっつける
だめな例defaults = {'color': 'red', 'user': 'guest'} parser = argparse.ArgumentParser() parser.add_argument('-u', '--user') parser.add_argument('-c', '--color') namespace = parser.parse_args([]) command_line_args = {k: v for k, v in vars(namespace).items() if v} d = defaults.copy() d.update(os.environ) d.update(command_line_args)辞書が大量にコピーされる
↓良い例d = collections.ChainMap(command_line_args, os.environ, defaults)コピーせずにもとの辞書をそのまま持つ
コードの可読性向上
関数のkeyword引数
だめな例twitter_search('@obama', False, 20, True)引数の意味がわからない
↓良い例twitter_search('@obama', retweets=False, numtweets=20, popular=True)NamedTuple
だめな例> doctest.testmod() (0, 4)0,4の意味がわからない
↓良い例> doctest.testmod() TestResults(failed=0, attempted=4)
TestResultsはTestResults = namedtuple('TestResults', ['failed', 'attempted'])で作れる
tupleのunpacking
だめな例p = 'Raymond', 'Hettinger', 0x30, 'python@example.com' fname = p[0] lname = p[1] age = p[2] email = p[3]↓
良い例fname, lname, age, email = p複数の状態を同時に更新する
だめな例def fibonacci(n): x = 0 y = 1 for i in range(n): print(x) t = y y = x + y x = t実行途中に状態が崩れている瞬間がある。行の順序を間違えやすい
↓良い例def fibonacci(n): x, y = 0, 1 for i in range(n): print(x) x, y = y, x+yこっちのほうが人間の思考に近い。
効率化(高速化、省メモリ)
文字列の結合
だめな例names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie'] s = names[0] for name in names[1:]: s += ', ' + name↓
良い例', '.join(names)リストの更新
だめな例names = ['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie'] del names[0] names.pop(0) names.insert(0, 'mark')遅い
↓良い例names = deque(['raymond', 'raychel', 'matthew', 'roger', 'betty', 'melisa', 'judith', 'charlie']) del names[0] names.popleft() names.appendleft('mark')速い
decoratorとcontext manager
- business logicとadministrative logicを分ける
- コードがきれいになる
- 正しく名前をつけないとグッチャになる
- With great power comes with great responsibility
キャッシュ
だめな例def web_lookup(url, saved={}): if url in saved: return saved[url] page = urlib.urlopen(url).read() saved[url] = page return page↓
良い例@lru_cache() def web_lookup(url): return urllib.urlopen(url).read()business logicとadministrative logicが分離している
一時的なコンテクスト
だめな例oldcontext = getcontext().copy() getcontext().prec = 50 print(Decimal(355) / Decimal(113)) setcontext(oldcontext)↓
良い例with localcontext(Context(prec=50)): print(Decimal(355) / Decimal(113))ファイルの開閉
だめな例f = open('data.txt') try: data = f.read() finally: f.close()↓
良い例with open('data.txt') as f: data = f.read()スレッドのロック
だめな例lock = threading.Lock() lock.acquire() try: print('Critical section 1') print('Critical section 2') finally: lock.release()↓
良い例lock = threading.Lock() with lock: print('Critical section 1') print('Critical section 2')エラーの無視
だめな例try: os.remove('somefile.tmp') except OSError: pass↓
良い例with ignored(OSError): os.remove('somefile.tmp')標準出力を一時的に置き換え
だめな例with open('help.txt', 'w') as f: oldstdout = sys.stdout sys.stdout = f try: help(pow) finally: sys.stdout = oldstdout↓
良い例with open('help.txt', 'w') as f: with redirect_stdout(f): help(pow)リストの内包記法
だめな例result = [] for i in range(10): s = i**2 result.append(a) print(sum(result))↓
マシな例print(sum([i**2 for i in range(10)])↓
良い例print(sum(i**2 for i in range(10)))
- 投稿日:2019-12-19T21:10:10+09:00
【Python】*args **kwrgs って何だろう
01.本記事に書くこと
- 関数のパラメータに定義する
*や**の意味と、その使い方時間が経ってしまっても素早く思い出せるように、ここに整理したいと思います。
02.
*と**は可変長引数を受け取るパラメータにつけるものである関数のパラメータに
*argsや**kwargsと書いてあるコードを見かけた。
これは可変長引数を受け取るパラメータに記述されるもので、関数呼び出し時にパラメータに渡す引数の数を、固定ではなく任意の数にできるようになる。可変長引数とは、通常1つしか引数が指定できないところ、任意数の引数を指定できるようにしたもの。
〜普通はダンボール1つにつき1つの品物しか入れられない。
しかし、可変長引数ならば1つのダンボールにつき、いくらでも品物を入れることができる上に、それらは1枚のプチプチでパックされる。〜例def kansu(*args): print(f'{args}を受け取った!') kansu('Apple', 'Banana', 100) # ('Apple', 'Banana', 100)を受け取った!パラメータは
args1つだけに対して、3つの引数を渡しているが、エラーなく動作している。身近な例でいうと、
print()関数や.format()メソッドでも可変長引数が用いられている。いくらでも引数を渡せるprint('a','b','c') # a b c又、
argsやkwargsという名前は慣習的に用いられるものなので、好きな名前でも動作する。def x(*z): y = [] for i in z: y.append(i*10) print(y) x(1,2,3,4,5,6,7,8) # [10, 20, 30, 40, 50, 60, 70, 80]03.
*が1つの時と2つの時の違い可変長引数には2種類あり、それぞれ引数の渡し方や、受け取り方が異なる。
1.可変長位置引数を受け取るパラメータ <-
*args
任意の数の引数をタプルで受け取る(引数を渡さなければ空のタプルになる)
位置引数で渡す〜全品物を1枚のプチプチでパックしてダンボールへ〜
2.可変長キーワード引数を受け取るパラメータ <-
*kwargs
任意の数のキーワード=値という形の引数を辞書型(key value)で受け取る
(引数を渡さなければ空の辞書になる)
キーワード引数で渡す〜全品物にポストイットで名前をつけた上、全品物を1枚のプチプチでパックしてダンボールへ〜
04.可変長
位置引数を受けとるパラメータの使い方引数に
*をつけることで、パラメータが可変長位置引数を受け取るようになる。
つまり、任意の数の引数を受け取る。
関数のパラメータのどこに可変長位置引数を定義するかによって、その関数の使い方が変わってくる。1.最後のパラメータが可変長def myfunction(x, y, *z): print(f'{x} - {y} - {z}') myfunction(2, 3, 5, 6, 7) # 2 - 3 - (5, 6, 7)1つ目、2つ目の引数はそれぞれ
x,yに対応している。
それ以降の引数はzに対応しており、タプルで格納される。2.途中のパラメータが可変長def myfunction(x, *y, z): print(f'{x} - {y} - {z}') myfunction(2, 3, 5, 6, 7) # TypeError: myfunction() missing 1 required keyword-only argument: 'z'パラメータ
zに何の引数も渡されていないため、エラーを吐く。この場合、zはキーワード専用引数になるので、デフォルト値を設定してあげたり、呼び出す時にキーワード引数として使わなければならない。2-1.デフォルト値を設定def myfunction(x, *y, z=0): print(f'{x} - {y} - {z}') myfunction(2, 3, 5, 6, 7) # 2 - (3, 5, 6, 7) - 02-2.キーワード引数として使うdef myfunction(x, *y, z): print(f'{x} - {y} - {z}') myfunction(2, 3, 5, 6, 7, z=0) # 2 - (3, 5, 6, 7) - 005.可変長
キーワード引数の使い方引数に
**のつけることで、パラメータが可変長キーワード引数を受け取る。
つまり、任意の数のキーワード=値を受け取り、キーワードと引数は、それぞれを辞書型のkeyとvalueに対応する。
関数のパラメータのどこに**を定義するかによって、その関数の使い方が変わってくる。1.最後のパラメータを可変長キーワード引数にするdef myfunction(param1, param2, **param): print(f'{param1} | {param2} | {param}') myfunction(2, 3, param3=4, param4=5, param5=6) # 2 | 3 | {'param3': 4, 'param4': 5, 'param5': 6}可変長キーワード引数で得た値は辞書型になる。
2.途中のパラメータを可変長キーワード引数にするdef myfunction(param1, **param, param2): print(f'{param1} | {param2} | {param}')結果はシンタックスエラー
可変長キーワード引数を途中に定義することは許できない。06.2つの可変長引数を併用する場合
可変長位置引数を受け取るパラメータの後においた通常のパラメータは、キーワード専用引数になる。
def Test(a, *b, c): # cはキーワード専用 pass可変長キーワード引数を受け取るパラメータの後に、通常のパラメータはおけない。
05-2.再掲def myfunction(param1, **param, param2): print(f'{param1} | {param2} | {param}')よって併用する場合は、以下のような順番にしなければならない。
1.位置引数とキーワード引数の両者を受け取るパラメータ 2.可変長位置引数を受け取るパラメータ 3.キーワード専用引数を受け取るパラメータ 4.可変長キーワード引数を受け取るパラメータ
- 投稿日:2019-12-19T21:02:15+09:00
Pythonで入力された整数を素因数分解 ver.1
はじめに
以前作成した「Pythonで入力された数字が素数か判定するプログラム」に、合成数だった場合に素因数分解を行ってくれる機能を追加してみました。
プログラムの原理
小学生の時に習った素因数分解のやり方を思い出して、小さい素数から順に割り続ける方針にしました。
具体的には、SymPyのprimerangeを用いて素数リストを作成し、そのリストから割り切れる数で割り続けます。そして、素数リストの最後まで割り算を行ったら終了としました。実際のプログラム
from sympy import primerange inn = n = int(input("素数か確かめたい数を入力してください。>> ")) primlist = list(primerange(2,(inn+1) / 2)) #1 yaku = [] for i in primlist: while n % i == 0: #2 n /= i yaku.append(i) #3 if not yaku: #4 print(n, "は素数です。") else: print(inn, "は合成数で、素因数分解すると",yaku, "です。")大まかな流れ
- 素数リストを (n+1)/2 まで作成
- 素数リストの中から割り切れる数を探し、割り切れなくなるまで整数 n を割り続ける。
- 割るたびに yaku というリストに割った素数を追加していく。
- yaku に何も入っていなければ素数と表示する。なにか入っていれば素因数分解した結果を表示。
実際にやっていること
- #1で、前回同様Sympyを用いて作成。
- #2で、while構文を用いることで、ifを使わずに直ぐに割り続ける動作に入れるようしてあります。
- #3で、.append(i) とすることで yaku に割った素数の i を追加します。
- #4で、yaku に数が入っていれば、合成数であるということに注目しました。具体的には if 構文を用いて、if not yaku とすることで yaku に何も入っていない際に、if より下の処理に入ります。
苦労した点
はじめにプログラムした際、primerange()で作成する素数リストの範囲を、前回と同じ「primerange(2, int(n**(1/2)) + 1)」としていました。すると、入力された整数が "2×素数" の場合に2のみが yaku に追加されて正確に素因数分解できない問題が発生しました。 具体的には14 (2×7) のような数字を入力した際に結果として『14 は合成数で、素因数分解すると [2] です。』と出てしまいました。
そのため素数リストの範囲を「primerange(2, (inn+1) /2)」に設定しました。こうすることで先程の問題の解決としました。この際、inn に +1 をすることで "2×素数" の整数でも素数リストが素数まで作られるようになっています。具体的には14を入力した際にprimerange(2, 7.5)となるため、primelistの中身が [2, 3, 5, 7]となります。もし+1をしないと、primerange(2, 7)となるため、primelistの中身が [2, 3, 5]となってしまい、7が素因数分解の結果に入らないことになり、正確な素因数分解ができなくなります。反省点
素数リストの作成の範囲が広すぎるために、入力される整数が大きくなる(7桁以上)と、とたんに計算速度が落ちます。そのため範囲の設定や素因数分解の方式を少し改善したい思います。
最後まで読んで下さいまして有難うございました。
- 投稿日:2019-12-19T21:01:42+09:00
Flask/Pythonからhtmlの埋め込み変数にリストを渡す
htmlの埋め込み変数{{hogehoge}}にPython/Flaskからリストを渡して表示させる際に[ ]が表示されてしまいます。
[ ]を非表示にすることが出来るようでしたら方法を教えて頂きたく。
よろしくお願いします。
- 投稿日:2019-12-19T20:37:11+09:00
PythonでCSV,TSVファイルをSQLiteにインポートする方法
はじめに
PythonでSQLiteのDBにCSVやTSVデータをインポートしたい!
そんな方のための、スクリプト一発でインポートする方法の解説です。SQLiteとはなにか、どうやってセットアップするのか等は以下の記事を参考にしてください。
PythonとSQLiteで実現するローカルでお手軽SQL環境作成術インポート方法
使い方はこの下の「解説」にて。
スクリプト
import_data_to_sqlite.pyimport sqlite3 import csv import os #######↓ここのパラメータを変える↓####### dbname = '' target_table_name = '' import_table_name = '' is_create_table = is_header_skip = ##################################### #######↓インポート先のテーブルDDL↓####### sql_script = """ """ ####################################### class ImportSQLite(): def __init__(self, dbname, target_table_name, import_data_name, is_create_table, is_header_skip=False, sql_create_table=None): """ csvまたはtsvファイルをSQLiteへインポートする :param dbname: text 接続先DB名 :param target_table_name: text インポート先となるDB上のテーブル名 :param import_data_name: text インポートしたいデータ名 :param is_create_table: boolean インポート先となるテーブルを作成するか否か :param is_header_skip: boolean インポートするデータのヘッダーを読み飛ばすか否か :param sql_create_table: text インポート先となるテーブルのDDL """ self.dbname = dbname self.target_table_name = target_table_name self.import_data_name = import_data_name self.is_create_table = is_create_table self.is_header_skip = is_header_skip _, raw_delimiter = os.path.splitext(import_data_name) if raw_delimiter == '.csv': self.delimiter = ',' elif raw_delimiter == '.tsv': self.delimiter = '\t' else: raise ValueError('Import file should be csv or tsv.') if is_create_table: if not sql_create_table: raise ValueError('It\'s necessary of sql to create table') else: self.sql_create_table = sql_create_table def read_import_file(self): with open(self.import_data_name, 'r', encoding='utf-8') as f: reader = csv.reader(f, delimiter=self.delimiter) if self.is_header_skip: header = next(reader) return [i for i in reader] def pick_column_num(self, import_data): """ インポートファイルの列数を算出する :param import_data: array(two-dimensional) :return: int """ columns = [] for raw in import_data: columns.append(len(raw)) if len(set(columns)) == 1: return columns[0] else: raise ValueError('this import files has diffrenect column numbers.') def insert_csv_file(self): input_file = self.read_import_file() column = self.pick_column_num(input_file) val_questions = ['?' for i in range(column)] cur.executemany("insert into {0} values ({1})".format(self.target_table_name, ','.join(val_questions)), input_file) if __name__ == '__main__': sql = ImportSQLite( dbname=dbname, target_table_name=target_table_name, import_data_name=import_table_name, is_create_table=is_create_table, is_header_skip= is_header_skip, sql_create_table=sql_script ) conn = sqlite3.connect(sql.dbname) cur = conn.cursor() if sql.is_create_table: cur.execute('drop table if exists {};'.format(target_table_name)) cur.execute(sql.sql_create_table) sql.insert_csv_file() conn.commit() conn.close()解説
設定項目
変数名 型 設定方法 dbname Text 接続先のDB名。パスの形で指定する。 target_table_name Text インポート先となるDB上のテーブルの名前 import_table_name Text インポートしたいデータの名前。パスの形で指定する。 is_create_table Boolean(True or False) インポート先となるテーブルを作成するか否か is_header_skip Boolean(True or False) インポートするデータのヘッダーを読み飛ばすか否か 諸注意
- インポート先となるテーブルを作成する場合、作成クエリをセットしないとエラーで落ちます。
- csv,tsv以外のファイルをインプットとして指定すると弾かれます。
使用法とサンプル
例えば以下のようなCSVがあって、接続先のDBにはまだテーブルがなかった場合
students.csvid,name,class,blood_type 1,Mike,Moon,B 2,Bob,Song,A 3,Gonzalez,Star,AB 4,Alex,Moon,設定項目はこのようになります。
(冒頭)import_data_to_sqlite.py#######↓ここのパラメータを変える↓####### dbname = 'test.db' # '/home/user/test.db'といった形で指定も可能 target_table_name = 'students' import_table_name = 'students.csv' # '/home/user/students.csv'といった形で指定可能 is_create_table = True is_header_skip = True ################################# #######↓インポート先のテーブルDDL作成↓####### sql_script = """ create table students( id integer, name text, class text, blood_type text ); """ #######################################あとは普通に起動するだけ
kick.sh$ python import_data_to_sqlite.pyおしまい。
- 投稿日:2019-12-19T20:36:34+09:00
PythonとSQLiteで実現するローカルでお手軽SQL環境作成術
はじめに
- ローカルにあるCSV(or TSV)ファイル、キー列でJOINしたり重複確認とかしたいなぁ...
- でも自由気ままに使えるDBなんてないし...
- そこまででかいデータでもないんだから、お手軽な方法ないかしら...
このようなことでお悩みのあなたへ、SQLiteというDBを使って、ローカルで簡単にDBを立ててデータをインポートしSQLを発行できる方法をご案内します。
※テキストファイルをインポートする方法については以下の記事にてご紹介!
PythonでCSV,TSVファイルをSQLiteにインポートする方法SQLiteってなに?
- インストールも簡単で軽量なDBMS
- 組み込み系デバイスにも頻出。IoTと相性Good
- DjangoなどのWeb用DBとしても用いられることも
- PostgreSQLやMySQLといったメジャーなDBMSとは期待される役割が違う
- Pythonに標準装備されている
そう!このSQLiteはPythonをインストールした時点ですでに使えるのです。
というわけで、ここからはPythonを使ってローカルにお手軽DBを作ってSQLを打ち放題にする方法をご紹介します!
(以下はPython3.7がローカルにあることを前提とします。Python3系であれば使えるはず)基本的な使い方
作成からアクセスまで
まずはDBの作成からアクセス、切断までの操作方法一覧。
connect.pyimport sqlite3 # 接続先となるDBの名前。'/home/user/database.db'といった表現方法も可能。 dbname = 'database.db' # コネクタ作成。dbnameの名前を持つDBへ接続する。 conn = sqlite3.connect(dbname) cur = conn.cursor() # ここから好きなだけクエリを打つ cur.execute('create table students(id integer, name text);') # 処理をコミット conn.commit() # 接続を切断 conn.close()DBにアクセスしたとき、もし指し先にDBファイルがなかった場合自動でDBが作成されます。
「DBが作成される」とはSQLite用のバイナリファイルが作成されるということを意味します。つまり接続先となるDBはただのファイルであり、接続先の表現方法はパスでそのファイルの位置を表現するということになります。
これはファイルをそのまま移動(mv)や複製(cp)すれば、どこでもデータを持ち運べるということでもあります。
拡張子は自由ですが、ここでは慣例で".db"を使っています。
テーブル操作方法
ctl1.py# CREATE TABLE cur.execute('create table students(id integer, name text, class text)') # 列追加 cur.execute('alter table students add column blood_type text') # 行追加 cur.execute("insert into students values(1, 'Mike', 'Moon', 'B')") # 連続したレコードの追加 students_data = [(2, 'Bob', 'Song', 'A'), (3, 'Gonzalez', 'Star', None)] cur.executemany("insert into students values (?, ?, ?, ?)", students_data) # テーブル消去 cur.execute("drop table students")executemanyメソッドは便利です。上のサンプルではタプルの入った一次元配列ですが、二次元配列でも同様にインサートできます。
(Python上での処理結果をそのまま流し込むのにも使えそう)ctl2.py# 値の取得方法1 table = cur.execute('select * from students') data = table.fetchall() print(data) # 値の取得方法2 for data in cur.execute('select * from students'): print(data)テーブルのデータを取得したいときは、selectしたあとfetchall()をします。イテレータとしても使えます。
取り出した値はタプルあるいはタプルを要素とした配列になっています。
CSV,TSVファイルをインポートする方法
以下をご覧ください!
PythonでCSV,TSVファイルをSQLiteにインポートする方法おしまい。
参考文献
- 投稿日:2019-12-19T20:13:59+09:00
東京大学大学院情報理工学系研究科 創造情報学専攻 2007年度冬 プログラミング試験
2007年度冬の院試の解答例です
※記載の内容は筆者が個人的に解いたものであり、正答を保証するものではなく、また東京大学及び本試験内容の提供に関わる組織とは無関係です。出題テーマ
- 経路問題
問題文
※ 東京大学側から指摘があった場合は問題文を削除いたします。
配布ファイル
※ 公開されていないので以下は筆者が適当に作ったものです
test001.txt(図1を想定)
4 (0,1)- (1,1)- (2,1)- (1,1)| (2,1)| (1,2)- (3,2)- (3,2)| (1,3)- (2,3)-test002.txt(図2を想定)
4 (2,0)| (2,1)- (1,1)| (2,1)| (3,1)| (1,2)- (1,2)| (3,2)| (0,3)- (1,3)- (2,3)- (3,3)- (3,3)|test003.txt(図2に扉を一個追加)
4 (2,0)| (2,1)- (1,1)| (2,1)| (3,1)| (1,2)- (1,2)| (3,2)| (0,3)- (1,3)- (2,3)- (3,3)- (3,3)| (0,2)*(3,3)(1), (2)
- ヒントより区切りがない場合全マスを使った一本道となることがわかる
- このときパネルはこの一本道に沿うので一本道と同じ数立てる
(n-1)**2
(3)
3-1
4
(0,1)- (1,1)- (2,1)-
(1,1)| (2,1)|
(1,2)- (3,2)-
(3,2)|
(1,3)- (2,3)-3-2
4
(2,0)|
(2,1)-
(1,1)| (2,1)| (3,1)|
(1,2)-
(1,2)| (3,2)|
(0,3)- (1,3)- (2,3)- (3,3)-
(3,3)|(4)
class Panel(object): def __init__(self, x, y, s): self.x = x self.y = y self.s = s def __repr__(self): return '({0},{1}): {2}'.format(self.x, self.y, self.s) def format_txt(text): text = text.strip() txt_split_by_space = text.split() ret = [] for txt_panel in txt_split_by_space: x = int(txt_panel[1]) y = int(txt_panel[3]) s = txt_panel[5] ret.append(Panel(x, y, s)) return ret def solve4(file_path): with open(file_path, 'r') as f: n = int(f.readline()) row = 2 * n + 1 maze =[[' ' for _ in range(row)] for _ in range(row)] # init maze # 格子点 for i in range(len(maze)): for j in range(len(maze)): if ((i % 2 == 0) and (j % 2 == 0)): maze[i][j] = '+' # 外枠 for i in range(len(maze)): if (i % 2 == 1 and i > 0): maze[0][i] = '-' maze[len(maze)-1][i] = '-' maze[i][0] = '|' maze[i][len(maze)-1] = '|' data = f.readlines() panels = [] for text in data: tmp = format_txt(text) panels.extend(tmp) for panel in panels: x = panel.x y = panel.y s = panel.s if s == '-': maze[y * 2][x * 2 + 1] = s if s == '|': maze[y * 2 + 1][x * 2] = s for maze_row in maze: row_txt = '' for mark in maze_row: row_txt += mark return maze def printmaze(maze): for maze_row in maze: txt = '' for maze_mark in maze_row: txt += maze_mark print(txt)(5)
from collections import deque def init_maze2graph(maze): n = int((len(maze) - 1)/2) node_num = n**2 graph = [[] for _ in range(node_num)] for i in range(n): for j in range(n): node_id = i * n + j y = i * 2 + 1 x = j * 2 + 1 if maze[y][x-1] == ' ': # left graph[node_id].append(node_id-1) if maze[y-1][x] == ' ': # up graph[node_id].append(node_id-n) if maze[y][x+1] == ' ': # right graph[node_id].append(node_id+1) if maze[y+1][x] == ' ': # down graph[node_id].append(node_id+n) return graph def bfs(graph): node_num = len(graph) # 0: 未発見, 1: 発見, 2: 到達 color = [0 for _ in range(node_num)] ret = [] for start in range(node_num): if (color[start] == 2): continue q = deque([start]) color[start] = 1 area = 0 while (len(q) > 0): u = q.popleft() color[u] = 2 area += 1 for v in graph[u]: if color[v] == 0: q.append(v) color[v] = 1 ret.append(area) return ret def solve5(file_path): maze = solve4(file_path) graph = init_maze2graph(maze) areas = bfs(graph) return sorted(areas)[::-1](6)
from collections import deque from math import sqrt def init_maze2graph(maze): n = int((len(maze) - 1) / 2) node_num = n**2 graph = [[] for _ in range(node_num)] for i in range(n): for j in range(n): node_id = i * n + j y = i * 2 + 1 x = j * 2 + 1 if maze[y][x - 1] == ' ': # left graph[node_id].append(node_id - 1) if maze[y - 1][x] == ' ': # up graph[node_id].append(node_id - n) if maze[y][x + 1] == ' ': # right graph[node_id].append(node_id + 1) if maze[y + 1][x] == ' ': # down graph[node_id].append(node_id + n) return graph def dijkstra(graph): node_num = len(graph) # 0: 未発見, 1: 発見, 2: 到達 color = [0 for _ in range(node_num)] inf = int(1e9 + 7) dist = [inf for _ in range(node_num)] parent = [-1 for _ in range(node_num)] s = 0 q = deque([s]) color[s] = 1 dist[s] = 0 while (len(q) > 0): u = q.popleft() color[u] = 2 for v in graph[u]: if color[v] == 2: continue if dist[v] > dist[u] + 1: dist[v] = dist[u] + 1 parent[v] = u q.append(v) color[v] = 1 return dist, parent def node_id2cord(node_id, n): x = node_id % n y = int(node_id / n) return x, y def solve6(file_path): maze = solve4(file_path) graph = init_maze2graph(maze) dist, parent = dijkstra(graph) node_num = len(graph) n = int((len(maze) - 1) / 2) if (dist[node_num - 1] > node_num): return "cant achieve" e = node_num - 1 root = [e] while (e != 0): p = parent[e] root.append(p) e = p root = root[::-1] ret = '(0, 0)' for i in range(1, len(root)): node_id = root[i] x, y = node_id2cord(node_id, n) ret += ' ({0}, {1})'.format(x, y) return ret(7)
class Node(object): def __init__(self, Id, x, y, isDoor): self.Id = Id self.x = x self.y = y self.isDoor = isDoor def __repr__(self): return 'id: {0}, ({1}, {2}), isDoor: {3}'.format(self.Id, self.x, self.y, self.isDoor) class Door(object): def __init__(self, node1, node2): self.node1 = node1 self.node2 = node2 def __repr__(self): return '({0}, {1})'.format(self.node1, self.node2) def cord2node_id(x, y, n): return x + y * n def format_txt2(text, n): text = text.strip() txt_split_by_space = text.split() panels = [] doors = [] for txt_panel in txt_split_by_space: if ('*' in txt_panel): x1 = int(txt_panel[1]) y1 = int(txt_panel[3]) x2 = int(txt_panel[7]) y2 = int(txt_panel[9]) id1 = cord2node_id(x1, y1, n) id2 = cord2node_id(x2, y2, n) node1 = Node(id1, x1, y1, True) node2 = Node(id2, x2, y2, True) door = Door(node1, node2) doors.append(door) else: x = int(txt_panel[1]) y = int(txt_panel[3]) s = txt_panel[5] panels.append(Panel(x, y, s)) return panels, doors def inputdata(file_path): with open(file_path, 'r') as f: n = int(f.readline()) row = 2 * n + 1 maze =[[' ' for _ in range(row)] for _ in range(row)] # init maze # 格子点 for i in range(len(maze)): for j in range(len(maze)): if ((i % 2 == 0) and (j % 2 == 0)): maze[i][j] = '+' # 外枠 for i in range(len(maze)): if (i % 2 == 1 and i > 0): maze[0][i] = '-' maze[len(maze)-1][i] = '-' maze[i][0] = '|' maze[i][len(maze)-1] = '|' data = f.readlines() panels = [] doors = [] for text in data: tmp_panels, tmp_doors = format_txt2(text, n) panels.extend(tmp_panels) doors.extend(tmp_doors) for panel in panels: x = panel.x y = panel.y s = panel.s if s == '-': maze[y * 2][x * 2 + 1] = s if s == '|': maze[y * 2 + 1][x * 2] = s for maze_row in maze: row_txt = '' for mark in maze_row: row_txt += mark return n, maze, doors def init_graph(n, maze, doors): node_num = n**2 graph = [[] for _ in range(node_num)] for i in range(n): for j in range(n): node_id = i * n + j y = i * 2 + 1 x = j * 2 + 1 if maze[y][x - 1] == ' ': # left node = Node(node_id - 1, j - 1, i, False) graph[node_id].append(node) if maze[y - 1][x] == ' ': # up node = Node(node_id - n, j, i - 1, False) graph[node_id].append(node) if maze[y][x + 1] == ' ': # right node = Node(node_id + 1, j + 1, i, False) graph[node_id].append(node) if maze[y + 1][x] == ' ': # down node = Node(node_id + n, j, i + 1, False) graph[node_id].append(node) for door in doors: graph[door.node1.Id].append(door.node2) graph[door.node2.Id].append(door.node1) return graph def dijkstra2(graph): node_num = len(graph) # 0: 未発見, 1: 発見, 2: 到達 color = [0 for _ in range(node_num)] inf = int(1e9 + 7) dist = [inf for _ in range(node_num)] dfnode = Node(-1, -1, -1, False) parent = [dfnode for _ in range(node_num)] s = 0 node_s = Node(0, 0, 0, False) q = deque([node_s]) color[s] = 1 dist[s] = 0 while (len(q) > 0): node_u = q.popleft() color[node_u.Id] = 2 for node_v in graph[node_u.Id]: if color[node_v.Id] == 2: continue if dist[node_v.Id] > dist[node_u.Id] + 1: dist[node_v.Id] = dist[node_u.Id] + 1 parent[node_v.Id] = node_u q.append(node_v) color[node_v.Id] = 1 return dist, parent def solve7(file_path): n, maze, doors = inputdata(file_path) graph = init_graph(n, maze, doors) dist, parent = dijkstra2(graph) node_num = len(graph) if (dist[node_num - 1] > node_num): return "cant achieve" node_e = Node(node_num-1, n - 1, n - 1, False) root = [node_e] while (node_e.Id != 0): node_p = parent[node_e.Id] root.append(node_p) node_e = node_p root = root[::-1] doors_set = set() for door in doors: doors_set.add('({0},{1})*({2},{3})'.format(door.node1.x, door.node1.y, door.node2.x, door.node2.y)) doors_set.add('({0},{1})*({2},{3})'.format(door.node2.x, door.node2.y, door.node1.x, door.node1.y)) i = 0 ret = '' while (i < len(root)): if i < len(root) - 1: node1 = root[i] node2 = root[i+1] txt_id1 = '({0},{1})*({2},{3})'.format(node1.x, node1.y, node2.x, node2.y) txt_id2 = '({0},{1})*({2},{3})'.format(node2.x, node2.y, node1.x, node1.y) isDoor = ((txt_id1 in doors_set) or (txt_id2 in doors_set)) if (isDoor): i += 2 ret += '({0},{1})*({2},{3}) '.format(node1.x, node1.y, node2.x, node2.y) else: i += 1 ret += '({0},{1}) '.format(node1.x, node1.y) else: node = root[i] ret += '({0},{1}) '.format(node.x, node.y) i += 1 return ret[:-1]感想
- うーん、面白みはないけど試験としてはきちんとしているなぁという印象
- 本質は幅(深さ)優先探索とダイクストラ(コストが全て1だからダイクストラもどき)という基本的なことを聞いているだけだけど、それだと簡単すぎるからグラフをいかにしてinput fileから作るかが相当面倒になっている
- 実装力、実装スピードに重きをおいた年だと個人的に思う
- NodeクラスのisDoorは完全に無駄実装しちゃいました笑
- 投稿日:2019-12-19T19:51:53+09:00
Pythonは楽ですわ
Pythonその4アドベントカレンダー19日目投稿致します。昨日は薬の副作用で書けませんでした。申し訳ございません。
Pythonを初心者に勧めるわけ
なぜなら簡単にプログラムが書けるからです。
最初からJavaやC#だとクラスの概念から教えることになるので一番大変です。
自分も実は23の頃クラスが書けなくて悩んだ時期がありました。
今では難なくかけるのにねwww。初心者はなぜブロック言語に頼るのか
それは正直言うとコードに違和感があるからです。
自分たちは正直一番最初がCOBOLだっただけにコードが怖かったら正直何もできませんでした。
実際自分は一回落ちこぼれになりました。
それからですね書くことが重要だと思うようになったのは。
ブロックプログラミングは比較的楽にかけますが。
壊れてしまったときに何も対応できません。
Micro::Pythonを教えたいという方には必ず伝えることがあります。
必ずPython本一冊は写経しろ!!
それはなぜかと言うとソースが壊れたときに直せるからです。
自分もMashupAwardでPepper賞頂いた経験があるのですが実はPython書けませんでした。
Choregrapheって実はPythonのブロックプログラミングをソフトなんですわ。
相方にPythonianがいたので直してもらいました。
ここで最初にPythonに触れたわけです。
今ではサクッと書いちゃう人間になったわけですが。サクッとかけると何がいいのか
実はサクッとかけると実は色んなことが自動化できます。
Excelに自動的に物を書いてみたり自動的にニュースを取得したり緊急情報の取得をするのに役に立ちます。
実は今回のアーバンデータチャレンジのアイデア部門で出すものですが実際にアプリ化する予定です。
自動取得の部分にはPython使うと決めてます。
他の言語だとめんどいからじゃないです。
一番使いやすいから。
C#も好きなんだけどPythonもjQuery・RIOT・ECMAScript・PHPもねって感じですね。
Pythonでプログラミングして解雇された人の話を見ましたが正直言っとくとやって正解なんです。
だけどおおっぴらにしちゃだめ。自分だけにとどめときましょう。会社に腹は立つでしょうけど所詮我々は1サラリーマンでしかないんだから。
とかいておきます。
今だったら一般職に言っただバリバリ書いてだんまりで提供しちゃうでしょうけど。
俺は一般職にはもう戻りませんよ。
お金にならないもん。
技術あってなんぼですから。そういえば隠れPythonプログラマに言いたい来年こそはハッカソンに来てほしい。
特に田村淳さん隠れてないで来年の中京テレビハッカソンに来てください。
実は大ぴらにもうWebで書いちゃったんだから正直ハッカソンに来てよと言いたいです。
ほんま隠れPythonian本当に多いので一回ハッカソンに出ると人生変わるかもよ。
と書いておきます。では。
- 投稿日:2019-12-19T19:25:09+09:00
[Python]配列の要素を更新
- 投稿日:2019-12-19T19:18:33+09:00
Numpy (n,)と(n,1)表記の違いについて【横ベクトルか縦ベクトルかの違い】
- 投稿日:2019-12-19T19:12:31+09:00
Google SpreadSheetから設定値を読み取るgssetting
aratana Advent Calendar 2019の20日目です。
最近、設定ファイルにサイト情報を書いて、チェックしにいくというスクリプトを動作させているのですが、チェック対象サイトが増えるたびに、設定ファイルを自分で書き換えるという作業が発生しているので、改善したいなと思ってきました。
現状だいたい下記のような流れです。
マネージャーががんばる -> サイトオープン -> サイトオープンしたよ連絡 -> シートにサイト情報を記入 -> 私に連絡が来る -> 私がサーバに入って、設定ファイルに反映私の作業は数分ですが、めんどうだなーって思ってきました。
スプレッドシートに書いてあるサイト情報をマスタにして、スクリプトから取得すれば解決だな!と思ったので、スプレッドシートから設定値を楽に取得するパッケージを作りました。Pythonで。gssetting
安直ですが、Google SpreadSheet Settingの略で、gssettingと命名してみました。
設定情報のモデルを作成し、それを元にシートから情報を取ってくるイメージです。
gspread依存です。2019/12/20現在、急ぎで作ったのでアルファ扱いで、完成度は低いですが、今後改善していきます。(テストファイルとかつくらなきゃ
利用方法
はじめに
まず、サービスアカウントを作成して、対象のシートに設定してください。
下記サイトをご参考に。。。
PythonでGoogleスプレッドシートを編集インストール
PyPiに登録しているので、ピップ!します。(現在は、Python3.7以上対象となってます。
pip install gssettingモデル作成
下記シートから、name列とvalue列のみを抽出しようかと思います。
from dataclasses import dataclass from gssetting import GSSetting @dataclass class Setting(GSSetting): username: str value: str headers = ["name", "value"]実行
下記コードを実行することにより、読み込み完了です。
後はいい感じに加工したりしていけばおkですね!from dataclasses import dataclass from gssetting import GoogleSpreadSheetSetting, GSSetting @dataclass class Setting(GSSetting): username: str value: str headers = ["name", "value"] if __name__ == "__main__": gs_setting = GoogleSpreadSheetSetting( "./service_account.json", "document_id" ) settings = gs_setting.load("sheet_name", "A1:C3", Setting) print(settings) for setting in settings: print(setting.username.value)実行結果
[Setting(name=<Cell R2C1 '私'>, value=<Cell R2C3 'ひらがな'>), Setting(name=<Cell R3C1 '僕'>, value=<Cell R3C3 '漢字'>)] 私 僕まとめ
楽に、シートから設定を取得できるパッケージは完成したので、色々なところで使いまわししていこうかなと思ってます。
- 投稿日:2019-12-19T18:40:50+09:00
手認識が実用レベルに到達した件
MediaPipe(Google)のHandTrackingで、Issueで話題になっているジェスチャー実装について書きます。
こんな感じで動きました
MediaPipe
今年6月のCVPR2019でMediaPipeの発表があり、
そのアプリケーションのひとつとしてHandTrackingが公開されました。
MediaPipeはMLパイプラインと呼ばれるコードを書かなくても
ビジュアライズツールだけでMLアプリケーションを構築するフレームワークです。ROSをやったことがある人はツールをイメージしやすいと思います。
HandTracking以外にも顔認識、ヘアセグメンテーション、物体認識に物体追跡のアプリケーションが公開しています。HandTrackingのマルチプラットフォーム対応状況
下記の環境で動作します。
- デスクトップ(C++)
- iOS(ObjC)
- Android(Java)
- WebAssembly('19年度末に公開予定)
実際にデスクトップ(Linux)と、iOSで動作を確認しました。
HandTrackingを直ぐに試したい人はWebAssembly版のDemoが試せます。このHandTrackingの何がすごいのか?
実際にHandTrackingが使えるレベルになるには認識精度と処理速度の両方の実現が必要です。
一般に認識精度と処理速度はトレードオフの関係で両立するのは大変です。アプローチとして、手のひらの認識に絞ることで、指を含めるアプローチと比べアンカー(領域)の数を3~5分の1に減らし、かつ高精度を達成したそうです。
結果として、計算量を減らせ、高速でかつ高精度を達成できたそうです。詳しくはブログを参照ください。
On-Device, Real-Time Hand Tracking with MediaPipe手認識は何処で使えるか?
PCの画面操作、VR空間で使えます。
- 仮想オブジェクトとのインタラクション
- ジェスチャーベース制御インターフェース
- 手話の理解
これらタスクに対応するには、ジェスチャーが必須です。
しかし、MediaPipeのHandTrackingではジェスチャーの実装予定はないそうです。ジェスチャーを実装してみる
今回、Issue#40にある距離索引の実装を試しました。
ブログでは姿勢推定(PoseNet)で試していますが、ジェスチャーでも使えるそうです。
実装は、ベクトルをスケールして、L2 normalizationし、コサイン類似度で求めるという内容です。
最後にVPTreeで探索しましたが。。。どうもうまくいきません。もう一度HandTrackingの実装を思い出します。
今回の手認識はふたつのモデルが使われています。
- 手のひら検出器モデル(BlazePalm) palm_detection.tflite
- 手の目印モデル hand_landmark.tflite処理手順としては、
1. 手のひらの方向と領域を識別
2. 方向に合わせ回転させた後、領域から手の目印を推論「あー回転ね、回転したらいいのか」ということで実装したところ最初の検証結果となりました。
導入編
環境はMacで構築しました。
なお、Python版のHandTrackingですが、本家の実装ではないので精度はいまひとつです。
了承のうえお試しください。導入に際してですが、MediaPipeではTensorflowを拡張した実装があります。
今後Tensorflowでも実装予定のようですが、ビルドが必要で導入が手間です。
なのでそこは飛ばしてカスタムしたモデルを導入しました。
palm_detection_without_custom_op.tfliteコマンドラインで構築
$ git clone https://github.com/metalwhale/hand_tracking $ cd hand_tracking $ wget https://raw.githubusercontent.com/wolterlw/hand_tracking/optical_flow/hand_tracker2.py // 回転前の値を取得する為の変更点 $ diff -u hand_tracker2.py.1 hand_tracker2.py --- hand_tracker2.py.1 2019-12-19 18:14:51.767858700 +0900 +++ hand_tracker2.py 2019-12-19 18:14:32.905475000 +0900 @@ -269,6 +269,7 @@ Minv = np.linalg.inv(Mtr) kp_orig = (self._pad1(reg) @ Minv.T)[:,:2] hand['joints'] = kp_orig + hand['base_joints'] = reg return hand def __call__(self, img, hands=None): $ wget https://gist.githubusercontent.com/otmb/d8837508d2694b11fbbda8229b9bb4ec/raw/5f8088239110549d0cab527699f8770b50990c87/hand_gesture.py $ pip3 install opencv-python tensorflow $ pip3 install scikit-learn $ pip3 install vptree $ mkdir gestures // gesturesフォルダに適当なジェスチャ画像を追加後実行 $ python3 hand_gesture.pyiOSでジェスチャー導入したかったのですが今のところAPIから回転が取得できないようです。
Access hand landmarks position in iOS #237おわりに
今回は、ジェスチャー実装を試しました。
ついにか、ようやくかHandTrackingが実用レベルに到達しました。
来年(2020年)は様々な活用が出てきそうですね!
- 投稿日:2019-12-19T18:09:43+09:00
Heroku×Redis×Python で始める NoSQL DB 入門
はじめに
Heroku では Redis という DB が公式で提供されています。
キーバリュー型データストア Redis をクラウドで | Heroku
これは SQL やテーブル設計が不要なデータベースで、
例えば Python の場合、以下のように手軽に扱えます。>>> conn.set('タイトル', 'Redisで始めるDB入門') # データの格納 True >>> conn.get('タイトル') # データの取得 'Redisで始めるDB入門'この記事では Redis の導入方法から、基本的な使い方までを紹介します。
データの永続化とデータベースの基本
興味があれば読んでください(クリックで開く)
消えたら困るデータをどこかに保存しておかなければならない
というのは、プログラミングを続ける上で直面しやすい問題です。
変数はプログラムが終了すると消えてしまいますからね。そのようなデータの保存は一般的に
データの永続化と呼ばれ、主に
JSONなどのファイルにテキストとして保存する方法と、
データベース管理システム(DBMS)を利用する方法があります。単純なデータの保存であればファイル管理でも良いのですが、
複雑なデータ構造を扱う場合や、複数人が同時に操作するような場面だと難しく、
データ管理に特化した DBMS の方が扱いやすくなります。また、Herokuでは基本的にファイルを一時的にしか保存できないため、
DB を利用せざるを得ない場面も多いと思います。この記事では簡単のため、 DBMS もまとめて DB と表記します。
DBの種類
いくつかありますが、主なカテゴリの
RDBとNoSQLの2種類を紹介します。RDB
関係データベース(Relational DataBase)の略で、MySQL や PostgreSQL など、
世の中で使われている DB や解説されている DB の多くは RDB です。
初学者にとっては、SQLの学習コストやテーブル設計の難しさが壁になります。Heroku では PostgreSQL が公式 Add-on で提供されています。
Postgres - SQL データベース・サービス | HerokuNoSQL
NoSQLは(No SQLではなく)Not only SQLの略で、
SQL を使わずに、Pythonの文字列やリストなどを扱うような感覚で、
シンプルにデータを管理できるという魅力があります。
NoSQLというカテゴリの中にも様々な種類があり、
例えば MongoDB はドキュメント型で、Neo4j はグラフ型です。
今回使う Redis はキーバリュー型(Key-Value-Store : KVS)です。Redis
オンメモリで読み書きが高速、永続化も可能な KVS です。
主にキャッシュのために使われることが多いですが、
最近ではランキングデータに使われるという話もよく見かけます。
StringListSetHashSortedSetなどの
様々なデータ型をサポートしています。
An introduction to Redis data types and abstractions – Redis
Heroku Redis の初期設定
Heroku のアカウント登録、アプリ作成、クレカ登録は以下から済ませておいてください。
プラン毎の定額課金方式のため、予期せぬ請求の心配はないです。Heroku Redis の Free プランは25MBが上限で、
自動バックアップは対応していないという制限があります。
障害などでデータが失われるリスクがある点は留意しておいてください。参考:Heroku Redisについてまとめてみた - Qiita
Heroku Redis のインストール
以下から、作成したアプリに Heroku-Redis をインストールします。
Heroku Redis - Add-ons - Heroku Elements右上の
Install Heroku Redisをクリック。
Add-on planがHobby Dev - Freeになっていることを確認してから、
App to provision toに作成したアプリ名を入力し、
Provision add-onをクリックすると、2~3分程でインストールが完了します。
REDIS_URLの確認
インストールが完了すると、Dashboard->Settings->Config Vars に
環境変数REDIS_URLが設定されています。Redisに接続する際に使います。
このURLは絶対に外部に公開しないでください。
使用状況の確認
Dashboard の Overview または Resources の
Heroku Redisから、
使用状況を確認することができます。
Pythonで操作する
以下の内容は
python3.8.0とredis-py 3.3.11で動作確認しています。
redis-py のインストール
簡単に操作を行うためのライブラリを導入します。
ローカルで試す場合は以下をターミナル(コマンドプロンプト)で実行してください。
python3 -m pip install --upgrade pip # 念の為実行しておく python3 -m pip install redis # redis-pyのインストールHeroku 上で実行する場合は
requirements.txtにredisを追加する必要があります。requirements.txtredis==3.3.11Redisに接続する
ここで環境変数
REDIS_URLを使います。
Heroku上で実行する場合はそのままで大丈夫ですが、
ローカルで実行する場合は設定しておくか、redis://~を直接書いてください。まず接続用のモジュールを用意しておきます。
r.pyimport redis import os def connect(): return redis.from_url( url=os.environ.get('REDIS_URL'), # 環境変数にあるURLを渡す decode_responses=True, # 日本語の文字化け対策のため必須 )以下、これをPythonインタプリタで使っていきます。
※ コマンドプロンプトにpython3やpy -3と入力すると立ち上がる実行環境$ python3 >>> import r >>> conn = r.connect() # このconnを通じて操作する基本操作
Redis では 1つのKey に 1つのValue が紐付くという形が基本です。
Keyは必ず文字列ですが、Valueは文字列以外の指定も可能です。
ただしデータ型によって使うメソッドが異なります。文字列型データの格納
set()メソッドに Key と Value を指定して格納します。
例えば、Key がタイトルで Value がRedisで始めるDB入門の場合は>>> result = conn.set('タイトル', 'Redisで始めるDB入門') >>> print(result) True # 格納に成功すると Trueという形になります。
文字列型データの取得
get()メソッドにKeyを指定して取得します。>>> result = conn.get('タイトル') >>> print(result) 'Redisで始めるDB入門' # Keyに紐付くValueが返るまた、引数に存在しない Key を指定した場合は
>>> result = conn.get('title') >>> type(result) <class 'NoneType'>のように None が返ります。エラーは発生しません。
Keyの存在確認
Key が登録されているかを確認するには
exists()メソッドを使います。>>> conn.exists('タイトル') 1exists メソッドの引数には複数のKeyを指定することが可能で、
そのうち登録されているKeyの数が返ってきます。>>> conn.exists('title', 'id', 'tag') 0 # 指定したKeyは全て登録されていないので各操作の前にこのメソッドで確認しておくと安全です。
Keyの一覧を取得
今現在どの Key が格納されているかを確認するには
keys()メソッドを使います。>>> conn.keys() ['タイトル']Keyの変更
Key を別の文字列に変更したい場合は
rename()かrenamenx()メソッドを使います。
第1引数に変更前の Key を、第2引数に変更後の Key を指定します。>>> conn.rename('タイトル', 'title') True # 変更に成功 >>> conn.renamenx('title', 'タイトル') True # 変更に成功これらは変更先の文字列が存在する場合に挙動が変わります。
>>> conn.set('title', '変更しないでね') # 変更先のKeyを登録 True >>> conn.renamenx('タイトル', 'title') False # 変更できない >>> conn.rename('タイトル', 'title') True # 上書きしてしまう >>> conn.get('title') 'Redisで始めるDB入門' # 'タイトル' に紐付いていた Value >>> conn.get('タイトル') None # 変更前のKeyは消滅しているKeyの削除
Key を削除したい場合は
delete()メソッドを使います。紐付く Value も消えます。
複数の Key を指定可能で、削除に成功した Key の数が返ります。>>> conn.delete('title') 1また、全ての Key を削除したい場合は
flushdb()メソッドを使います。>>> conn.flushdb() True集合型の場合
Redisで扱えるデータ型には様々ありますが、
特に便利な集合型(Set)について特筆します。Redis の集合型は Python の集合型と同様の性質で、
要素の重複が許されない順不同な集合データです。集合型データの格納・取得
文字列型では
set()とget()を使いましたが、
集合型ではsadd()とsmembers()を使います。>>> conn.sadd('タグ', 'Heroku', 'Redis') 2 # 追加できた要素の数が返る >>> conn.smembers('タグ') {'Redis', 'Heroku'} # 順不同なので追加した順番と違う
sadd()は第一引数に Key を、第二引数以降に集合型データの要素を指定します。集合型データに要素を追加・削除
要素の追加にも
sadd()を使います。>>> conn.sadd('タグ', 'Python') 1 >>> conn.smembers('タグ') {'Redis', 'Heroku', 'Python'}ただし、既にある要素は追加できません。
>>> conn.sadd('タグ', 'Redis', 'redis-py') 1 # 'redis-py' は追加されるが 'Redis' は重複するので追加されない >>> conn.smembers('タグ') {'Redis', 'Heroku', 'redis-py', 'Python'}要素の削除には
srem()を使います。>>> conn.srem('タグ', 'Heroku', 'Redis') 2 >>> conn.smembers('タグ') {'redis-py', 'Python'}Key の削除は文字列型と同様に
delete()メソッドを使います。その他のデータ型や操作について
記事にはまとめきれない多くのデータ型や操作があります。
それらは公式リファレンスに記載されています。
Welcome to redis-py’s documentation! — redis-py 2.10.5 documentationそして redis-py で使えるメソッド名は Redis 公式のコマンド名に準拠しています。
Command reference – Redis
ただしdelは Python の予約語と衝突するのでdeleteになっています。また、様々な疑問を自力で解決するのは難しいので、
Python や各種技術に関する情報交換ができるコミュニティを紹介しておきます。
Python.jp Discordサーバ - python.jp最後に
大規模なシステムで全てのデータを NoSQL で管理するのは狂気の沙汰です。
いずれは RDBMS の偉大さを学んでください。また、上記の内容を活用した例を紹介しているので、
興味があれば読んでみてください。
NoSQL×PaaSで運用するナレッジベース+WebAPI - Qiita
- 投稿日:2019-12-19T18:08:41+09:00
NoSQL×PaaSで運用するナレッジベース+WebAPI
これはなに
NoSQLとPaaSでナレッジベース+WebAPIを構築した際のノウハウのまとめです。
文字列タグ指向無向グラフ型ナレッジベースというアーキテクチャを実装する場合に、
Heroku + Redis + FastAPI で構築した例と、
AWS(DynamoDB + Lambda + API Gateway) で構築した例を紹介します。コード部分は全て
Python3.8.0を使用しています。※ 現在AWS編が未完成です。ごめんなさい。
ナレッジベースとは?
ナレッジベースには様々な定義がありますが、この記事では
「知識をコンピュータが読み取り可能な形式で格納したデータベース」を指します。
知識ベースナレッジデータベースKBなどの呼ばれ方もあります。
文字列タグ指向無向グラフ型ナレッジベース
今回例として構築するナレッジベースです。
名前だけでは分かりにくいと思うので、イメージ図を用意しました。
(可視化は未実装のためマインドマップツール coggle で作成)なお、このアーキテクチャは流通しているものではなく、
GraphQLから着想を得て独自に設計したものです。
(サーベイはしていないのでもしかしたら既存かもしれません)「文字列タグ指向」について
このナレッジベースでは文字列(とその集合)データのみを扱い、
全ての文字列をタグとして扱います。上の図の例では、
Webサービス名アカウントIDURL記事タイトル概念プログラミング言語
などの各文字列を1つのタグとして扱っています。仕様上、文字列には空白や改行文字などは含まないものとします。
「無向グラフ型」について
このナレッジベースでは、関係のあるタグを結び付けるようにします。
例えば
フレームワークというタグには
RailsLaravelDjangoFlask
というタグが紐付いているというデータが取得可能で、例えば
QiitaとPythonのどちらのタグも紐付いているのは
https://qiita.com/1ntegrale9/items/94ec4437f763aa623965
というタグ(QiitaのPythonに関する記事URL)、のようにデータを取得できます。上の図では頂点(文字列)がタグ、辺が関係を表しています。
そして無向なので双方参照が可能です。
また包含関係などは考慮しないので、重み付けはありません。参考記事:グラフ理論の基礎 - Qiita
このナレッジベースの役割
「2つの文字列を格納する」という単純な操作の繰り返しだけで、
辞書的な知識体系(集合知)を形成することです。そして爆速で育て上げるためにWebAPIが必要になります。
構築例:Redis + FastAPI + Heroku
手軽に無料で運用したい場合はこちらを採用します。
Heroku の初期設定や Redis の基本操作はこちらで解説しています。
Heroku×Redis×Python で始める NoSQL DB 入門 - QiitaRedis
オンメモリで読み書きが高速なKVSです。永続化にも対応しています。
1つのタグに複数のタグを紐付けたいので、集合型のみを使います。ライブラリのインストール
Python で扱うため、redis-py を利用します。
python3 -m pip install redis hiredishiredis-py は C 実装の高速なパーサのラッパーです。
redis-py 側が hiredis を検知してパーサを切り替えてくれるので入れておきます。Redisに接続
以下のコードで接続の初期化を行います。
Heroku Redis が自動で設定してくれる環境変数REDIS_URLを使います。import redis, os conn = redis.from_url(os.environ['REDIS_URL'], decode_responses=True)デフォルトだと日本語の表示に不具合があるので、
decode_responses=Trueは必須です。全てのタグを取得
keys()を使って取得します。def get_all_tags(): return sorted(conn.keys())タグが一覧で見れると便利なので用意しておきます。
ただ規模が大きくなると負荷が高くなるので注意が必要です。紐付くタグを取得する
smembers(key)使って取得します。def get_related_tags(tag): r.smembers(tag) if r.exists(tag) else []念のため、存在しないタグを指定された場合は空配列を返すようにします。
存在確認にはexists(key)を使います。2つのタグを紐付けて格納する
sadd(key, value)を使って集合型データを格納します。
双方向に紐付けたいので、key-value を入れ替えて2回実行します。def set_relation_tags(tag1, tag2): return conn.pipeline().sadd(t1, t2).sadd(t2, t1).execute()Redis はトランザクションをサポートしており、redis-py の場合は
pipeline()からexecute()までのチェーンにすることで、
トランザクション内での一括実行ができます。また、pipeline メソッドによるアトミックな実行は、個別実行よりも高速のようです。
PythonでRedisを効率的に使う(redis-pyのパフォーマンスをあげるには) - [Dd]enzow(ill)? with DB and PythonFastAPI
FastAPI は Python の Web フレームワークの1つで、
シンプルな WebAPI を少ないコードで実装できるのと、
API ドキュメントを設定なしで自動生成してくれるのが特徴です。Flask Responder Starlette DRF などはオーバースペックで、
Bottle は逆に機能が足りず、FastAPIがちょうどよくハマりました。ライブラリのインストール
python3 -m pip install fastapi uvicorn email-validatorUvicorn は高速なASGIサーバーです。FastAPIの起動に使います。
Gunicorn の typo ではないです。email-validator は入れておかないと起動時に怒られます。何故か。
アプリケーションの初期化
非常にシンプルです。
main.pyfrom fastapi import FastAPI app = FastAPI()引数の
titleとdiscriptionを設定すると、
上記画像のような自動生成される API Doc にタイトルと説明部が反映されます。main.pyapp = FastAPI( title='collective-intelligence', description='文字列タグ指向無向グラフ型ナレッジベース', )また、
docs_urlを指定すると、API Doc のURLを変更できます。
デフォルトでは/docsですが、ルートにしておくのも良いでしょう。main.pyapp = FastAPI(docs_url='/')全てのタグを取得
シンプルに HTTPメソッド(GET) と URL と返り値を書くだけです。
リストか辞書を返り値にすることで JSON レスポンスになります。main.py@app.get('/api') def read_all_tags(): return get_all_tags()この定義が API Doc に自動で反映されます。
更に右上のTry it outからリクエストを実行できます。指定のタグに紐付くタグを取得
タグには記号も含む任意の文字列を想定しており、
クエリストリングでは対応できないため、POST にしておきます。main.py@app.post('/api/pull') def read_related_tags(tag: str): return get_related_tags(tag)引数に指定した
tag: strをリクエストボディから受け付けます。
型アノテーションを付けていますが、これを利用してリクエストのバリデーションを行います。
適合しない場合、422 Validation Errorがレスポンスになります。2つのタグを紐付けて格納する
FastAPI は pydantic という、
型アノテーションを活用するためのライブラリを内包しています。
これを利用して独自の型を定義し、バリデーションに利用します。main.pyfrom pydantic import BaseModel class Tags(BaseModel): tag1: str tag2: str @app.post('/api/push') def create_tags_relationship(tags: Tags): set_tags_relationship(tags.tag1, tags.tag2) return {tag: get_related_tags(tag) for _, tag in tags}定義した型は Schema として API Doc に反映されます。
FastAPI の起動
先ほど紹介した Uvicorn で起動します。
main.py内のappで初期化した場合はmain:appと指定します。
--reloadオプションにより、ファイル変更時にリロードして反映してくれます。$ uvicorn main:app --reload INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Started reloader process [57749] INFO: Started server process [57752] INFO: Waiting for application startup. INFO: Application startup complete.
http://127.0.0.1:8000またはhttp://127.0.0.1:8000/docsにアクセスすると、
API Doc が表示されることが確認できると思います。Heroku
Webアプリケーションを手軽にデプロイできるPaaSです。
多くの言語・フレームワークに対応しており、
PostgreSQL や Redis も一定枠まで無料でホスティングしてくれます。最初に以下の手順が必要です。
- アカウント登録 Heroku | Sign up
- カード登録 Account · Billing | Heroku
- アプリ作成 Create New App | Heroku
- Redisアドオン追加 Heroku Redis - Add-ons - Heroku Elements
必要なファイルの用意
以下のファイルが必要になります。
これを GitHub リポジトリに用意します。$ tree . ├── main.py # アプリケーション ├── Procfile # プロセス実行コマンド定義ファイル ├── requirements.txt # 依存ライブラリ定義ファイル └── runtime.txt # Pythonバージョン定義ファイルProcfileweb: uvicorn main:app --host 0.0.0.0 --port $PORTrequirements.txtfastapi email-validator uvicorn redis hiredisruntime.txtpython-3.8.0実際のディレクトリ も参考にしてください。
アプリケーションのデプロイ
Dashboard の Deploy タブからデプロイ作業を行います。
GitHub と連携してリポジトリを紐付け、Manual Deployを実行します。
Automatic deploysも設定しておくと、master への push 時に自動でデプロイしてくれます。build が無事に完了したら、
登録されたプロセスをConfigure Dynosから ON にしておきます。Dashboard 右上の
Open appからデプロイされたアプリケーションを確認できます。構築例:AWS(DynamoDB + Lambda + API Gateway)
執筆中のため公開をお待ちください
スケーラビリティを意識するならこちらを採用します。
柔軟にデータ構造を変えることも可能です。初めての、LambdaとDynamoDBを使ったAPI開発 - Qiita
API Gateway + Lambda + DynamoDB - QiitaAmazon DynamoDB
RDBと同様に1テーブル1プライマリキーが基本です。
プライマリキーはデータを一意に識別するためのキーで、「パーティションキー」または
「パーティションキーとソートキーの複合キー」のことです。
ソートキーを追加することによってパーティションキーのユニーク制限を緩和できます。開始方法 - Amazon DynamoDB | AWS
初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~ | Developers.IO
無料枠で頑張るためにDynamoDBのキャパシティを理解する - ITと筋トレの二刀流テーブルの設計
パーティションキー:タグ
ソートキー:timestampテーブルの作成
AWS Lambda
初めてのサーバーレスアプリケーション開発 ~LambdaでDynamoDBの値を取得する~ | Developers.IO
GitHub Actionsを使ってAWS Lambdaへ自動デプロイ (詳説+デモ手順付きver) - Qiita2つのタグを紐付けて格納する
Lambda が呼び出されると lambda_handler 関数が実行される
import boto3, time from decimal import Decimal def lambda_handler(event, context): timestamp = Decimal(time.time()) table = boto3.resource('dynamodb').Table('collective-intelligence') with table.batch_writer() as batch: # 複数putする場合はbatch_writerを使うと良い batch.put_item(Item={ 'tag': event['tag1'], 'related_tag': event['tag2'], 'timestamp': timestamp }) batch.put_item(Item={ 'tag': event['tag2'], 'related_tag': event['tag1'], 'timestamp': timestamp }) return {'statusCode': 201}指定のタグに紐付くタグを取得
import boto3 from boto3.dynamodb.conditions import Key def lambda_handler(event, context): table = boto3.resource('dynamodb').Table('collective-intelligence') response = table.query(KeyConditionExpression=Key('tag').eq(event['tag'])) # tag指定で検索 tags = set(item['related_tag'] for item in response['Items']) # set型に格納して重複を削る return {'statusCode': 200, 'body': list(tags)} # JSONレスポンスのためlist型にキャストAmazon API Gateway
WebAPIの作成と管理をしてくれる
初めてのサーバーレスアプリケーション開発 ~API GatewayからLambdaを呼び出す~ | Developers.IO
ゼロから作りながら覚えるAPI Gateway環境構築 | Developers.IO
Amazon API Gateway チュートリアル - Amazon API Gatewayリソースとメソッドの作成
/push と /pull で POST を作成
リクエストの検証を設定
Lambdaを実行する前に弾けるとコストが下がってよい
- モデル(JSON Schema)の定義
- 設定->リクエストの検証に「本文の検証」を設定
- リクエスト本文にモデルを設定
JSON Schema Tool
リクエストおよびレスポンスマッピングのモデルおよびマッピングテンプレートを作成する - Amazon API Gateway
APIGatewayの新機能Request Validationを試してみた - エムティーアイ エンジニアブログメソッドの作成
メソッドの選択
メソッド管理画面
PULLモデルの作成
PUSHモデルの作成
リクエストの検証を設定
PULL API のテスト
PUSH API のテスト
利用料金に関して
Billing 画面の請求書から確認します。
まだ本稼働しているわけではないですが、
テストで数百件のリクエスト/レスポンスの送受信を行った結果0円だったので、
お試しで使う分には恐れることはなさそうです。GCP vs AWS
GCP(Firestore)とAWS(DynamoDB)で悩みましたがDynamoDBを採用しました。
GCP側で選択する場合、4つのデータストアサービスから悩むことになると思いますが、
趣味で使うならFirestore一択だと思います。
データベースを選択: Cloud Firestore または Realtime Database | Firebase終わりに
これらはほぼ独学で得た内容ですが、
新しい技術を習得するスキルは会社のモダンな環境で得られたものだと思います。
強いエンジニアが新技術をガンガン使っていく現場で働けるというのは最高の体験です。また、Heroku側の構成のものを公開しています。
公開時点でデータは空ですが、自由に触ってみてください。
https://collective-intelligence.herokuapp.com/
- 投稿日:2019-12-19T16:38:50+09:00
pandas.DataFrame で数値以外の要素の抽出
pandas.DataFrame で数値以外の要素の抽出
この記事は古川研究室 Advent_calendar 17日目の記事です。
本記事は古川研究室の学生が学習の一環として書いたものです.内容が曖昧であったり表現が多少異なったりする場合があります.
はじめに
pandas で前処理していたときに,データに数値以外の要素が混入していて歯がゆい思いをしたので,記事としてまとめます.
扱うデータ
kaggle の 2,2k+ Scotch Whisky Reviews データセット
データの説明
スコッチウイスキーについてレビュワーが評価しているデータセットです.
データ数 2247,項目数 7 のデータです.データの確認
# ライブラリの import import pandas as pd import numpy as np # csv ファイルの読み取り data = pd.read_csv('scotch_review.csv') # データの表示 data.head()本題
扱うデータに数値以外の型を持つ要素があるか確認
今回は項目の 'review.point'(評価点) と 'price'(価格) のみを使います.
それぞれの列のデータ型を見ていきます# 型の確認 data[['review.point','price']].dtypes'review.point'列に数値以外の型が混ざっていそうですね.
次のコードで該当する列に数値以外の要素があるか判定できます.# 'review.point'列に数値ではない型の要素があるか確認 data[['price', 'data']].apply(lambda s:pd.to_numeric(s, errors='coerce')).notnull().all()数値以外の型を持つ要素の抽出及び変換
ここからは 'price' 列から,数値以外の型を持つ要素を抽出し,置換していきます.
まずは抽出です.# 数値ではない型の要素の抽出 pic = data[['price']][data['price'].apply(lambda s:pd.to_numeric(s, errors='coerce')).isnull()] picここでは /set と /liter のデータは欠損値として扱い,他は数値の型に変更します.
data_c = data.copy() # 置換後のデータの確認 data_c['price'][pic.index] # 置換するデータの作成 change_data = [60000, 30000, np.nan, 1700, 1100, 60000, 3500, 4000, 1100, 7500, 1735, 1100, np.nan, 7500, np.nan, 1500, 1400, np.nan, np.nan] # コピーを作成し,置換 data_c = data.copy() data_c.loc[pic.index,'price'] = change_data # 置換後のデータの確認 data_c.loc[pic.index,'price']df = dropna()終わりに
今回は pandas.DataFrame で数値以外の要素の抽出し,置換をしました.次回の記事では,この前処理をした Whisky Reviews データセットを可視化してみます.
- 投稿日:2019-12-19T15:29:49+09:00
Python3エンジニア認定基礎試験 -備忘録と問題傾向-
Python3エンジニア基礎認定試験
■受験詳細
受験日:通年(パソコンスクールなどでも開催しているため試験会場を要確認)
申込URL:http://cbt.odyssey-com.co.jp/pythonic-exam.html
受験料金:1万円(税別)■試験概要
試験名:Python3 エンジニア認定基礎試験
(英名:Python 3 Certified Engineer Basic Examination)
資格名:Python3 エンジニア認定基礎試験合格者
(英名:Python 3 Basic Grammar Certification)
概要:文法基礎を問う試験
問題数:40問(すべて選択問題:1問25点計算)
試験時間:60分
合格ライン:正答率70%
出題範囲:オライリー・ジャパン「Pythonチュートリアル 第3版」および一般的知識■模擬試験URL
https://diver.diveintocode.jp/exam
筆者のプログラミングレベル
初心者です。
■Progato
・ruby
・python■Kaggle
・触りだけ■Rails tutorial
・最終14章まで完了模擬試験内容
ここからは問題を元に回答を記述していきます。
問1:仮想環境にインストールされたすべてのパッケージを表示するオプションを選択肢の中から選びなさい。
正解:
pip listpipはパッケージ管理で利用されている。
主として行えるのはパッケージの、
・インストール
・アップグレード
・リムーブ
そのほか様々なコマンドがあり、以下表に一部をまとめてみる。
subcommand 意味 install パッケージを指定してインストール可能
--updateで最新バージョンをインストール可能uninstall installの逆 freeze インストールされたパッケージのリスト表示(install向け表示) list 仮想環境にインストールされたすべてのパッケージを表示 show show パッケージ名でパッケージ情報を表示 問2:以下のプログラムを実行した際の出力結果を選びなさい。
正解:
0 1 2num_list = [2, 4, 6, 4, 4, 2, 6] for i in range(num_list.count(4)): print(i, end=' ')countは対象の数を数えるメソッド。
forで繰り返しているかつ end = ' ' が入ることによって、
0 1 2と表現される。問3:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。
正解:
Norodef dive_into_code(teacher, *mentor): print(teacher) dive_into_code('Noro', 'Nakao', 'Miyaoka')あぶれた位置引数('Nakao', 'Miyaoka')を(*menter)にまとめて代入している。
類似問題dic = [ ['Noro', 'Nakao', 'Miyaoka'], ['Kimura', 'Miyashita', 'Shibata'], ['Matsumoto', 'Tanaka', 'Ivan'], ] print(list(zip(*dic)))正解:
[('Noro', 'Kimura', 'Matsumoto'), ('Nakao', 'Miyashita', 'Tanaka'), ('Miyaoka', 'Shibata', 'Ivan')]問4:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。
模擬試験問題dive_into_code = [(1, 'Noro'), (2, 'Nakao'), (3, 'Miyaoka'), (4, 'Kimura')] dic = dive_into_code #1は第二要素を昇順にする。0なら第一要素で昇順にする。 dic.sort(key=lambda dic: dic[1]) print(dic)正解:
[(4, 'Kimura'), (3, 'Miyaoka'), (2, 'Nakao'), (1, 'Noro')]
なのでK→M→Na→Noでsortしているわけですね。問5:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。
正解:
xxxxCODExxxxDIVExxxxDIVEreplace.pyd = 'xxxxDIVExxxxDIVExxxxDIVE' print(d.replace('DIVE', 'CODE', 1))文字列.replace(置換前の文字列, 置換後の文字列, 最大回数)
問6:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。
正解:
3.142%全体の幅.小数点以下の幅f
format.pyprint("出力結果:") print('円周率は%5.3fである。'%math.pi) 円周率は3.142問7:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。
正解:
[('Noro', 'Kimura', 'Matsumoto'), ('Nakao', 'Miyashita', 'Tanaka'), ('Miyaoka', 'Shibata', 'Ivan')]zipを用いてリスト型:listを辞書型:dictionaryにする方法
zip.pydic = [ ['Noro', 'Nakao', 'Miyaoka'], ['Kimura', 'Miyashita', 'Shibata'], ['Matsumoto', 'Tanaka', 'Ivan'], ] print(list(zip(*dic)))以下を参考にしました。
https://pg-chain.com/python-dictionary-list以上です
- 投稿日:2019-12-19T14:43:48+09:00
KDDCUP2019で1位を獲得した研究提案の紹介
ドコモの落合です。この記事では,KDD2019で開催されたデータ分析のコンペティションであるKDD Cupの1部門で優勝した1取り組みについて紹介します。
KDD Cup 2019概要
KDDの概要は前回の記事で紹介したので,ここではKDD Cupにフォーカスして説明します。KDD CupはKDDに併設されたデータサイエンスのコンペティションで初回は1997年と20年以上の伝統があります。昨年まではKaggleなどの通常のコンペティションと同様に,データと課題が与えられ,何らかの予測を行い,その精度を競うというコンペでした。これまでもドコモR&DではKDD Cupに挑戦しており,2016年に初挑戦しファイナリストになったことがありました。
KDD Cup 2019では,従来の予測精度を競うコンペはRegular ML TrackのTask1として継続し,それ以外にも与えられたデータを使って自由に課題設定して研究提案するRegular ML Track Task2,機械学習の各工程(特徴抽出や予測モデル構築,検証など)を自動で行うAutoML Track,強化学習でマラリア感染の拡大を防ぐ方策を競うHumanity RL Trackが新設されました。ドコモでは全トラックに参加しており,Regular ML Track Task2で1位を獲得することができました。
https://www.kdd.org/kdd2019/docs/Winners_Regular_Baidu.pdfProposal Title: Simulating the Effects of Eco-Friendly Transportation Selections for Air Pollution Reduction
Keiichi Ochiai, Tsukasa Demizu, Shin Ishiguro, Shohei Maruyama, Akihiro Kawana
研究紹介ムービー:https://www.powtoon.com/online-presentation/bugFjP07kIK/eco-friendly-transportation-selectionsRegular ML Trackの詳細
Regular ML Trackは中国のBaiduが主催し,Baidu Mapsの乗り換え検索ログが提供されました。ログは4種類ありますが,ここではTask2で利用した3種類だけ説明します。
提供されたログの説明
検索クエリログ(公式ページより引用)
検索クエリのログは,sidがsession ID, pidがユーザID(実際はプライバシー保護のためある程度似たプロファイルの人で丸めている),req_timeが検索リクエストをした時刻,oが出発地の緯度経度,dが目的地の緯度経度です。
経路候補ログ
こちらも公式ページからの引用です。1つの検索クエリに対して,複数の経路(交通機関,交通モード)が提示されています。transport modeは交通機関を示すIDで,例えば1がバスで2が地下鉄で・・といった感じです。ただし,どのIDが実際のどの交通機関に対応しているかは公開されていませんでしたので様々な情報から推定しました。あとは,Distanceは距離,ETAは移動時間,estimated priceは利用料金です。このようなデータが1つのクエリに対して複数あります。
クリックログ
こちらも公式ページからの引用です。クリックログには,session IDと選択した交通モードが記録されています。sidをキーとして,ここまで述べた3つのログを紐づけることができます。
Regular ML Track Task1は,ここまで説明したログを元にユーザが利用する交通モードの予測を行うタスクでした。Task2は,上記のログを使って自由に課題設定を行い研究提案を行います。Task1は予測精度で評価できますが,Task2はそのような指標がないので,英語で4ページの論文を書いてCommittee memberが内容を評価するという通常の論文投稿のような形式で評価されました。
提案内容
私たちのチームでは,交通モードの選択と大気汚染の削減という環境問題を結びつけ提案を行いました。その詳細をここから説明していきます。
研究背景
環境問題は重要な社会問題の1つであり,CO2削減に向けた取り組みが世界規模で行われています。その一方,国連の発表によると2017年のCO2排出量が4年ぶりに増加に転じ,パリ協定の2℃目標(産業革命後の気温上昇を2℃以内に抑える)が難しいのではないかという意見もあります。
こうした状況を踏まえ,国や企業レベルの取り組みだけでなく個々人の取り組みも重要ではないかと考えています。個々人が環境問題に取り組む1つのアプローチとして,日々の生活で環境に優しい交通手段を選択するということが考えられます。しかしながら,単にCO2を排出する交通機関を利用しないようにするというのでは,人々にとって生活に支障が出るため,そのような行動は選択されないと考えられます。一方,生活に支障がない範囲であれば,CO2削減につながる交通手段を選択してもらえる可能性があります。交通手段を変えたときにどの程度CO2削減に貢献できるかわかった方が,ユーザーにとっても選択するモチベーションになると考えられます。しかし,効果を定量的に示すには人々の交通手段に関するログが必要になります。
そこで,この研究では,乗り換え検索サービスのログを使い,クリックした交通手段で実際に移動したと仮定することで,環境に優しい交通手段を選択した場合のCO2排出量の削減量のシミュレーションを行います。さらに,徒歩や自転車移動が増えることで健康にも良い影響があると考えられるので,健康への影響を定量的に評価します。
アプローチ
基本的なアイディア
あるユーザーに提示された経路候補ログから,各交通モードを利用したときのCO2排出量は以下の式で計算できます。
CO2排出量 = 移動距離(km)×単位距離・人あたりのCO2排出量(g/person・km)
移動距離は経路候補ログにあるので,単位距離・人あたりのCO2排出量がわかればCO2排出量が計算できます。そのデータは公益財団法人交通エコロジー・モビリティ財団が公開しているものがあり,それを利用します。例えば,BusとCyclingが経路候補の以下のログの場合,以下の赤枠のように計算できます。
また,クリックログからこのユーザーがBusをクリックしていたことがわかります。ここで,代替の交通モードを受け入れ可能ということを以下のように定義します。
代替ルートの移動時間(ETA) ≦ クリックしたルートの移動時間
代替ルートのCO2排出量 < クリックしたルートのCO2排出量1つ目の条件は移動時間が元々ユーザーが選択していた移動時間以下という条件です。環境に優しい交通モードは徒歩や自転車ですが,遠くまで歩くという候補が出ても選ばれないと考えられます。一方,元々選択していた交通モードの移動時間を超えなければ受け入れてもらえるだろうという想定です。2つ目の条件は,CO2排出量が減ることです。この受け入れ可能の定義に従うと,先ほど例ではCyclingが受け入れ可能となります。これを全検索ログに適用します。実際のログでは,数十万件の検索クエリに対して上記のような条件を満たす交通手段を探索するため,組合せ最適化問題 として定式化できます。
整数最適化問題としての定式化
交通モード選択を移動時間とCO2排出量の制約付きの0-1整数最適化問題として定式化します。最小化する目的関数と制約は以下のようになります。
ここで$P_{i,j}$はユーザー$i$が交通モード$j$を選択したときのCO2排出量,$Q_{i,j}$ はユーザー$i$が交通モード$j$を選択したときの移動時間, $Q\prime_{i,j}$はユーザー$i$がクリックした(実際に移動したと仮定した)交通モードの移動時間,$X_{i,j}$は選択した交通モードのみ1で他は0となるOne-hotベクトルのような値です。また,$m$はセッションID数,$n$は交通モード数を示します。実装の際にはCO2排出量と移動時間で単位が違うので両者を正規化しています。
最適化問題としての定式化には,大阪大学の梅谷先生のチュートリアルが非常に参考になりました。
組合せ最適化入門:線形計画から整数計画まで
https://www.slideshare.net/shunjiumetani/ss-17197023定式化できれば,解くのは既存のソルバーを利用します。今回はPuLPというライブラリを使いました。
結果
ユーザーのクリックログをベースラインとして,最適化後の結果と比較を行いました。結果は以下の表になります。約40万クエリの検索ログを対象に最適化を行いました。
CO2排出量は約9.23%削減できるようです。意外にも移動時間も9.96%削減できるという結果になりました。ユーザーは必ずしも最速経路をクリックしているわけではないのかもしれません。
次に,最適化によって交通手段がどう変化したか見ていきます。下の表で括弧内の左がユーザーがクリックした交通モード,右が最適化で選択された交通モードです。
公開されたログが北京中心部ということもあり,バスや地下鉄などで短距離を移動するケースが多かったようで,それを自転車に変えるという変更が多かったようです(表の赤枠)。一方,自家用車(Driving)では,自転車に変わっている件数が少なく自家用車のままが多いです(表の青枠)。車で移動するところは距離が遠く代替手段がないのかもしれません。
健康への影響については概要だけ説明します。Oxford大学の研究で,自転車に乗ることによる死亡率低下への影響を分析した研究があります(論文はこれ)。この研究によると,WHOが推奨している運動量(11.25 METh/weekの身体運動,150分の中程度の有酸素運動)をこなすと総死亡率が10%低下すると報告されています。今回のシミュレーションからユーザーは平均して1日あたり自転車に平均23.04分(WHO推奨の13.63%)乗ると計算できます。両者を組み合わせ,総死亡率を$10\% \times 13.63\% = 1.36\%$低下させる可能性があると考察しました。
交通手段の変化の結果を見てもわかりますが,今回の研究では自転車に置き換えられるところは自転車にするという結果になっています。しかし,実際にこれをやろうとすると駐輪スペースの問題があったり,天候によって受け入れられる交通手段が変わる(雨だったら自転車では移動したくない),どうやってユーザーにCO2が少ない交通手段を選んでもらうかというUI/UXなど,まだまだ実用までは課題があります。また,乗り換え検索でクリックした交通手段・ルートの通りに移動したと仮定していますが,実際にどうやって移動したのかわからないという根本的な制約もあります。
実装
最後に実装について少し紹介します。実装は以下の記事を参考にさせていただきました。
https://qiita.com/samuelladoco/items/703bf78ea66e8369c455
https://qiita.com/mzmttks/items/82ea3a51e4dbea8fbc17事前にc_co2[i,j]にCO2排出量,c_eta[i,j]に移動時間(ユーザーi, 交通モードj)が入った辞書を作成しておきます。例えばこんな感じです。
c_eta[2,1] = 1976.0, c_eta[2,3] = 1146.0, c_eta[2,4] = 1446.0, c_eta[2,5] = 2246.0, c_eta[2,6] = 818.0
この例では,ユーザIDが2番のユーザーが交通モード1,3,4,5,6が候補として提示され,各交通モードでの移動時間が上記のようになっているとします。import pulp problem = pulp.LpProblem("ETA-CO2 minimize", pulp.LpMinimize) x = {} # 0-1変数を定義(制約条件3のX_{i,j}が0,1をの2値という制約に対応) for i in I: for j in J: x[i,j] = pulp.LpVariable("x({:},{:})".format(i,j), 0, 1, pulp.LpBinary) # 目的関数を定義。移動時間とCO2排出量をそれぞれ最大値で割って正規化します。 max_co2 = max(c_co2.values()) max_eta = max(c_eta.values()) problem += pulp.lpSum( ((c_co2[i,j]/max_co2) * x[i,j]) + ((c_eta[i,j]/max_eta) * x[i,j]) for i in I for j in J if (i,j) in c_co2), "TotalCost" # 制約条件1: ユーザiについて、割り当ててよいモード数は1つ for i in I: problem += sum(x[i,j] for j in J if (i,j) in c_co2 ) == 1, "Constraint_leq_{:}".format(i) # 制約条件2: ユーザiがクリックした交通モードの移動時間より短い # click_log_dfにクリックログがpandasのdataframeとして入っている for n, t in click_log_df.iterrows(): i = int(t["sid"]) if (i,int(t["transport_mode"])) in c_co2: baseline = int(c_eta[i, int(t["transport_mode"])]*1.0) problem += sum(c_eta[i,j]*x[i,j] for j in J if (i,j) in c_co2 ) <= baseline, "Constraint_co2eq_{:}".format(i) # ソルバーを指定する solver = pulp.solvers.PULP_CBC_CMD() result = problem.solve(solver) # 最適化できたかどうかと,そのときの評価値を出力 print(pulp.LpStatus[result], pulp.value(problem.objective))タイムライン
KDD Cup 2019のRegular ML Trackは4/13頃に課題が公開されました。その後は以下のスケジュールで論文投稿まで行いました。
4/19 初回打ち合わせ(データ活用方法の案出し)
5/8 打ち合わせ#2(データ活用方法の案出し&方針決定)
5/15 打ち合わせ#3(最適化のプログラムを実装、この時点では最適化が収束せず)
5/31 打ち合わせ#4(最適化実装完了)
6/3〜10 論文執筆(英語4ページ)
6/11〜12 英文校正
6/13 投稿
6/15 投稿締め切り(後日締め切り延長されて6月末までに)2019年はゴールデンウィークに10連休があり,実質1ヶ月半程度で研究テーマ決め,実装,論文執筆まで行いました。
最後に
私自身は最適化問題には馴染みがなかったのですが,一緒に研究していた同僚が詳しく,定式化やPulpの実装,最適化がうまくいかないときの対応策について教えてくれました。また,別のメンバーが環境問題に応用しようという提案をしてくれたり,各交通モードがIDだけ提供されているところを,どのIDがどの交通モードなのかWebで公開されている運賃やデータの統計などを見ながら特定してくれたりとチームメンバーそれぞれが貢献してできた研究です。これからも面白い研究をして,学会発表などの対外活動を通じてドコモのデータ分析・AI分野の認知度を向上させたいと考えており,これからもKDDなどのトップカンファレンスに挑戦していきたいと思います。
それではみなさん,メリークリスマス&よいお年を!
世界最高峰のデータ分析競技会「KDD CUP 2019」で世界1位を受賞 https://www.nttdocomo.co.jp/binary/pdf/info/news_release/topics_190809_01.pdf ↩
- 投稿日:2019-12-19T14:20:27+09:00
データサイエンス学ぼう
Python
(未記載)
機械学習(入門)
- モデルの仕組み
- データ分析入門
- 機械学習の初モデルを作りましょう。
- モデルを検証しましょう(Model validation)
- Underfitting and Overfitting
- Random Forest
- まとめて練習宿題
機械学習(中級)
(未記載)
- 投稿日:2019-12-19T14:13:06+09:00
メモ:[Python3]日付を好きなフォーマットの文字列で取得
strftime()を使う
>>> from datetime import datetime >>> datetime.now().strftime('%Y%m%d-%H%M%S') '20191219-140750'https://docs.python.org/ja/3/library/datetime.html#strftime-and-strptime-format-codes
ISO 8601 形式
>>> datetime.now().isoformat() '2019-12-19T14:11:53.346885'
- 投稿日:2019-12-19T14:05:41+09:00
AWSでYOLOv3を動かす
目的
AWS上でYOLOを動かす。
AWSの知識があまりないため、解決策の1例としてご覧ください。使用したもの
[YOLO] https://github.com/ptxyasu/keras-yolo3
[AWS] EC2 https://aws.amazon.com/jp/ec2/流れ
1.AWSに環境構築
2.AWSに必要なデータ設置
3.yolo実行AWS EC2について
今回は開発環境と一致させるため、Ubuntu 16.04のAMIを選択。
インスタンスタイプは無料枠のt2.microとした。問題1 ボリューム不足
環境構築を行い、keras-yolo3をcloneしようとするとボリュームがないというエラーが発生。
解決策1 EBSのボリュームの変更
ボリュームのサイズを8GiBから少しずつ変更。
6時間に1回程度しか変更できないことに注意。しかし、増やすだけ料金がかかることにも注意!!
結果的に20GiBまで増やした。問題2 メモリ不足(おそらく)
エラー文を調べてもあまり情報は出てこず。
解決策2 インスタンスタイプの変更
t2.microの仮想CPU1つ、メモリ1GiBが問題かなと感じた。
そこでインスタンスタイプをt2.microからt3a.2xlargeへ変更。
これに関してはt2.largeやt2.xlargeへの変更でも良かったかも。
料金がすごく高くなってしまうので注意!問題3 yolo実行でのエラー
エラー文を読むとおそらく以下が問題。
AWS内にfontが存在しないのだと思う。yolo_video.pyfont = ImageFont.truetype(font='font/FiraMono-Medium.otf', size=np.floor(8e-3 * image.size[1] + 0.5).astype('int32'))解決策3 プログラムの変更
今回検出画像ではなく、クラス名とその座標が得られればいいので描写に関する部分は消すことで対応。
yolo_video.pydef detect_image(self, image): start = timer() if self.model_image_size != (None, None): assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required' assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required' boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size))) else: new_image_size = (image.width - (image.width % 32), image.height - (image.height % 32)) boxed_image = letterbox_image(image, new_image_size) image_data = np.array(boxed_image, dtype='float32') print(image_data.shape) image_data /= 255. image_data = np.expand_dims(image_data, 0) # Add batch dimension. out_boxes, out_scores, out_classes = self.sess.run( [self.boxes, self.scores, self.classes], feed_dict={ self.yolo_model.input: image_data, self.input_image_shape: [image.size[1], image.size[0]], K.learning_phase(): 0 }) print('Found {} boxes for {}'.format(len(out_boxes), 'img')) for i, c in reversed(list(enumerate(out_classes))): predicted_class = self.class_names[c] box = out_boxes[i] score = out_scores[i] predicted_value = self.get_concentration(predicted_class) value = max(value,predicted_value) label = '{} {:.2f}'.format(predicted_class,score) top, left, bottom, right = box top = max(0, np.floor(top + 0.5).astype('int32')) left = max(0, np.floor(left + 0.5).astype('int32')) bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32')) right = min(image.size[0], np.floor(right + 0.5).astype('int32')) print(label, (left, top), (right, bottom)) if top - label_size[1] >= 0: text_origin = np.array([left, top - label_size[1]]) else: text_origin = np.array([left, top + 1]) # My kingdom for a good redistributable image drawing library. end = timer() print(end - start) return image def close_session(self): self.sess.close()問題4 物体が検出されない
これまでの方法を行うことで、yoloの実行まではできた。
しかし、 "Found 0 boxes for img" となり、物体が認識されない、、解決策4
サーバーに置く学習モデルのミス? → モデルには問題なし
画像やプログラムを開く際の権限? → 権限を与えても変化なし
開発環境が複数GPUに対し、AWSでは複数CPU → 本来関係なさそうだが少し影響あるのかも(要検証)結果
実行までは至ったが、物体が検出されず。
ボリュームの拡大とインスタンスタイプの変更で料金が5000円程度も上がってしまったため、インスタンスを削除。検出されない原因の解明と最適なインスタンスタイプの検討をしてからまた取り組みます。
エラーの心あたりある方いたらコメント欲しいです!お願いいたします。
























































