20191219のPythonに関する記事は30件です。

学習記録 その7(11日目)

学習記録(10日目)

勉強開始:12/7(土)〜
使用書籍:大重美幸『詳細! Python3 入門ノート』(ソーテック社、2017年)

【手書き文字の分類(Ch.16 / p.396)】 から再開(10日目)、
【ボストンの住宅価格分析(Ch.16 / p.408)】 まで終了(11日目)【読了】

アヤメの分類

・hold out(学習データをある比率で分割し、訓練データとテストデータに分けること。)
 ただし、データの並びが均一でない場合はよい学習成果が出ない。
 その場合は shufflesplit を用いると、分割に合わせて要素のシャッフルが行われる。
・shufflesplitを用いて行う分析方法の1つに crossvalidation がある。
 手元のデータ数が十分でない時に、学習に使用するデータを毎回分割、違う塊を選択し、繰り返す方法

ボストンの住宅価格分析

・相関関係を予想する「回帰」処理を実施。
・書籍通りに進めていくと、どうもエラーが出る場所を発見。
 モジュールがインポートされていないことによる関数の未定義だった。
 最後のまとめにはちゃんと記載されていたが、途中の経過には記載がなかったため、何も考えずに進めていたら恐らく詰まる。
 気づいただけでも、ちょっとした成長を実感。

以上の内容をもって上記書籍は読了しました。

Progate講座

Qiitaスクリーンショット 2019-12-19 22.39.52.png

上記書籍を読了したものの、一番最初のこの段階でしっかりと基礎を固めておきたいと思い実施中です。
ガイドを受けつつ、1つのものを段階的に作りあげていくプログラムになっており、書籍使用による勉強とはまた違った側面でアウトプットを行えるので非常に勉強になっています。
Python学習コースは全部で5コースあり、今日一日で4まで終了しました。
Python以外にもJavaやRuby、Gitに関するコースまであったので、初心者には非常に有用だと感じました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

svmモジュールのSVCについて(数学は抜く。)

皆さんこんばんわ。
12/19の文を書いていきます。
今日は用事があって遅くなりましたね。。。

今日は昨日勉強した直線SVCモデルの分類をまとめていこうと思います。

分類の目的

2019_12_19_blog.png

上の写真を見てください。
分類とはこの青の点とオレンジのバツを分ける線を自動的に見つけることです。
2次元だと人間にとってとても簡単ですよね?
SVCモデルは高次元の場合でも分ける為の式を求めれられるらしいので凄いですよね。
さぁ、これが何に役に立つの?と思った方も多いでしょう。
下の画像はいかがでしょうか?

2019_12_19_picpng.png

違いがわかりましたか?
そう、Gメールでは当たり前のように迷惑メールを弾いてくれますが、これはAIの力なのです。(アルゴリズムはもっと複雑です。)
けど、やっていることは同じで、ある直線(面)を引いてスパムか有効なメールかを分析しているのです。
美しいし、凄いですよね。

さて、今日の最終の目的の画像を見せますね。
以下になります。

Figfdsaure_1.png

⚠︎注意したいことがあります。
それはこの例の場合だと、無限の線が引けると言うことです。
よって、この点線の幅(マージ)が一番広くなった上で、その真ん中に実線(決定境界)を引きます。

さてこの決定境界を求める為のコードを見ていきます。

サンプルコード

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を求めることくらいしか理解できてない。
それはもう少し勉強が進んで記事にしようと思う。

今日は夜が遅いのでこれくらいで。
おやすみなさい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

この処理Perlでどう書く?

この処理Pythonでどう書く? の二番煎じです。

実際perlだったらどう書くかなと考えながら練習がてら。できる限りCOREモジュール縛り。

この処理Perlでどう書く?

標準出力・標準エラー出力

# 標準出力にメッセージ
echo 'HELLO'

# 標準エラーにメッセージ
echo 'ERROR!' >&2
use 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 = 3
use 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};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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]が実在するとき次が成り立っていることが期待されます。

  1. 政党[x, y]yと友好的である(なぜなら前者は後者を守っているから)。
  2. 政党[x, y]xと友好的でない(なぜなら前者は後者から何かを守ろうとしているから)。

また元記事では以下の公理も仮定しています。

  • xyが友好的でありyzが友好的であるならば、xzも友好的である(味方の味方は味方)

いま「xyが友好的である」ことをx ~ yと表記すると、
上の公理は「関係~は推移的である」と言い換えることができます。

また、反射律(自分は自分と友好的)と対称律(xyが友好的ならばyxも友好的)も成り立つと考えられるため、関係は同値関係となっています。

よって、全ての政党は同値類で重複なく分割されることがわかります。

第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と国民が友好的であるような世界では政党などいらないと言うことなのでしょう。

政党が矛盾するとき

xyが同じ陣営だとすると、公理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 q

NHKから国民を守る党から国民から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

の漸化式で求められます。

〔補遺終わり〕

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pygameで風船割りゲーム

はじめに

前回の投稿では、tkinterを使ったターン制の三目並べを作成したので、今回はpygameを使った常に画面が更新されるものを作ってみました。
タイトルで風船割りゲームと書きましたがそこまでのゲーム性はありません。笑
ただ、今後ゲームを作る際に使えそう?な処理は学べたかなぁ。。。と思います。
例によって、欲しい機能を寄せ集めたキメラティックコードなので読みやすさは今後の課題ですね。

今回の完成品
balloon_game.gif

一番最後に今回のコード全体像を載せてあります。

表示画面作成

まずは基盤となる画面作成からですね。

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()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-yml

Dcokerfile

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: true

test.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'), ('.', '.')]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sample

sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

光彦「ボクなら自己紹介で犯人わかりますよ!」

プロローグ

みなさんこんにちは。
今日は、ボクが考えた事件解決の助けとなる画期的な発明を紹介したいと思います。
それは…事件が起きる前の容疑者たちの会話を集めて、犯人と被害者を予測するシステムです!
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%と低いですね、もしかしたら派手なセリフは犯人ではないと学習しているのかもしれません。

被害者の確率が最も高かったのはキラの「僕は新世界の神となる!」でしたね。

結論

夜神ライトは死ぬ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

QoreSDKを使ってBTCの価格上昇・下降を予測してみる

だいぶ投稿が遅くなってしまいましたがこちらの記事は深層学習以外の機械学習と応用技術 by QuantumCore Advent Calendar 2019の7日目の記事です。

はじめに

Qiita - QoreSDKの紹介とQoreで不整脈検出

まずはじめにこちらのQoreSDKの紹介記事読んで、Qoreの強みが時系列データに強く、学習が高速でデータが少なくても精度が出せるという点であるということを理解しました。
そこで通貨取引の価格上昇下降のタイミングが素早く検出できれば取引の利益を大きく損失を小さくすることができるのではないかと考えました。
次に扱う題材ですがBTC(ビットコン)に決めました。また、BTCの取引履歴はオープンデータとして公開されているため開発者が扱いやすいという利点も選択理由の一つです。

というわけでKaggleに公開されていますこちらのデータをKaggleDataset - bitcoin-historical-dataを今回は使います。

これ以降の紹介で使っておりますソースコードは下記GooglaColabratoryで動作確認したコードになります。
GoogleColabratory - btc.ipynb

データの読み込み

まずKaggleDatasetからデータをダウンロードしたことを前提に話を進めます。GoogleColabratory上でKaggleDatasetからデータをダウンロードする方法については下記記事を参考にしました。

Qiita - Google Colab上でKaggleのデータをロード、モデル訓練、提出の全てを行う

まずはデータを読み込む部分です、元々加工済みのCSVファイルということもありpandasでそのまま読み込めます。

load_data.py
import 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.py
btc[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)であったのでそのように変換しておきます。

preprocess2.py
data = 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.py
from 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.py
response = 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
"""

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

続 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

大まかな流れ

  1. 書類を複合機にてスキャン
  2. スキャンデータ(PDF)を画像データに変換する
  3. 変換した画像データを切り抜く
  4. 切り抜いたデータの2値化する
  5. OCRにかける

複合機にてスキャン(PDF)

本社にある複合機を利用します、スキャンすると登録されているメールアドレスにPDFが添付されてきます。
※最終的な運用ではサイマの各工場でスキャンをする予定

PDFから画像データへ変換する

スキャンされたデータはPDF形式なので画像データに変換します。
ディレクトリを指定すると格納されたPDFファイルを画像データに変換してくれるというものです。
pdf2imageをインポートしてconvert_from_pathというメソッドに変換したいファイルパスを渡せば
変換してくれます、簡単ですね。

pdf2png.py
from 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.py
from 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.py
import 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']
    }    

このスクリプトを実行するとこのような画像から
スクリーンショット 2019-12-19 22.11.02.png
※画像は加工してあります。

このように指定した座標で切り抜くことができました。
buyoption_1013.png

切り抜いたデータを2値化する

次は切り取った画像のOCR精度を高めるために文字を2値化して読み取り精度の向上を図ります。

opencvを利用して作成
2値化のプログラムはシンプルにこんな感じで

deeply_character.py
import 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)

切り取ったこんな画像が
buyoption_1013.png

こんな感じに2値化されました
buyoption_1013.png

サンプルが良くないのかあまり恩恵を感じられないです。

手強そうなこの画像で試してみようと思います。
sample.png

しきい値を調整して2値化すると・・・
buyoption_1013.png

なんと!よりはっきりとした画像になりました。

この画像を前回作成したGCPのOCRにかけてみます。
その結果・・・
スクリーンショット 2019-12-19 20.24.52.png

このように文字起こしされました、よく考えると「配達日」という箇所もノイズになるので省いても良かったですね。
ただこの精度ならチェックに関する再利用性が保てそうです。

まとめ

今回はOCRに掛ける前処理として、

  • 必要な箇所だけの切り抜き
  • 切り抜いた画像の2値化による鮮明化

をすることで如何にOCRに対して有利な状況を作れるかに挑戦してみました。

今回の事務処理効率化の取り組みですが事業部内で「やってみます!」と豪語したは良かったですが、納品書の現物を見ると本当に自動化できるのかと不安でした。
結果、画像の切り抜きによるノイズの除去と2値化による鮮明化の後でOCRにかけることで、精度が高めることでき、自動化が現実的になったかなと実感してます。

アドベントカレンダー2回にわたりサイマにおける納品書チェックという課題にOCRを中心とした技術で自動化へ挑戦をはじめました。今後はシステムの実装を進めつつ工場を巻き込んた運用の実現に取り組みたいです。

最後に

Ateam cyma Advent Calendar 2019の21日目いかがでしたか。
22日目はサイマのデザイナである@ryo_cyさんがBEMを用いたCSS設計について話をしてくれますのでお楽しみに!

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアで興味を持った方はcymaのQiita Jobsをご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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_longest

python2では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 i

for内で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)))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

又、argskwargsという名前は慣習的に用いられるものなので、好きな名前でも動作する。

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) - 0
2-2.キーワード引数として使う
def myfunction(x, *y, z):
    print(f'{x} - {y} - {z}')

myfunction(2, 3, 5, 6, 7, z=0) # 2 - (3, 5, 6, 7) - 0

05.可変長キーワード引数の使い方

引数に**のつけることで、パラメータが可変長キーワード引数を受け取る。
つまり、任意の数のキーワード=値を受け取り、キーワードと引数は、それぞれを辞書型のkeyvalueに対応する。
関数のパラメータのどこに**を定義するかによって、その関数の使い方が変わってくる。

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.可変長キーワード引数を受け取るパラメータ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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, "です。")

大まかな流れ

  1. 素数リストを (n+1)/2 まで作成
  2. 素数リストの中から割り切れる数を探し、割り切れなくなるまで整数 n を割り続ける。
  3. 割るたびに yaku というリストに割った素数を追加していく。
  4. yaku に何も入っていなければ素数と表示する。なにか入っていれば素因数分解した結果を表示。

実際にやっていること

  1. #1で、前回同様Sympyを用いて作成。
  2. #2で、while構文を用いることで、ifを使わずに直ぐに割り続ける動作に入れるようしてあります。
  3. #3で、.append(i) とすることで yaku に割った素数の i を追加します。
  4. #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桁以上)と、とたんに計算速度が落ちます。そのため範囲の設定や素因数分解の方式を少し改善したい思います。
最後まで読んで下さいまして有難うございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flask/Pythonからhtmlの埋め込み変数にリストを渡す

htmlの埋め込み変数{{hogehoge}}にPython/Flaskからリストを渡して表示させる際に[ ]が表示されてしまいます。
[ ]を非表示にすることが出来るようでしたら方法を教えて頂きたく。
よろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでCSV,TSVファイルをSQLiteにインポートする方法

はじめに

sqlite-183454_640.png

PythonでSQLiteのDBにCSVやTSVデータをインポートしたい!
そんな方のための、スクリプト一発でインポートする方法の解説です。

SQLiteとはなにか、どうやってセットアップするのか等は以下の記事を参考にしてください。
PythonとSQLiteで実現するローカルでお手軽SQL環境作成術

インポート方法

使い方はこの下の「解説」にて。

スクリプト

import_data_to_sqlite.py
import 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.csv
id,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

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとSQLiteで実現するローカルでお手軽SQL環境作成術

はじめに

  • ローカルにあるCSV(or TSV)ファイル、キー列でJOINしたり重複確認とかしたいなぁ...
  • でも自由気ままに使えるDBなんてないし...
  • そこまででかいデータでもないんだから、お手軽な方法ないかしら...

このようなことでお悩みのあなたへ、SQLiteというDBを使って、ローカルで簡単にDBを立ててデータをインポートしSQLを発行できる方法をご案内します。

※テキストファイルをインポートする方法については以下の記事にてご紹介!
PythonでCSV,TSVファイルをSQLiteにインポートする方法

SQLiteってなに?

sqlite-183454_640.png

  • インストールも簡単で軽量なDBMS
  • 組み込み系デバイスにも頻出。IoTと相性Good
  • DjangoなどのWeb用DBとしても用いられることも
  • PostgreSQLやMySQLといったメジャーなDBMSとは期待される役割が違う
  • Pythonに標準装備されている

そう!このSQLiteはPythonをインストールした時点ですでに使えるのです。
というわけで、ここからはPythonを使ってローカルにお手軽DBを作ってSQLを打ち放題にする方法をご紹介します!
(以下はPython3.7がローカルにあることを前提とします。Python3系であれば使えるはず)

基本的な使い方

作成からアクセスまで

まずはDBの作成からアクセス、切断までの操作方法一覧。

connect.py
import 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にインポートする方法

おしまい。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

東京大学大学院情報理工学系研究科 創造情報学専攻 2007年度冬 プログラミング試験

2007年度冬の院試の解答例です
※記載の内容は筆者が個人的に解いたものであり、正答を保証するものではなく、また東京大学及び本試験内容の提供に関わる組織とは無関係です。

出題テーマ

  • 経路問題

問題文

※ 東京大学側から指摘があった場合は問題文を削除いたします。
Screen Shot 2019-12-19 at 19.53.37.png
Screen Shot 2019-12-19 at 19.53.48.png

配布ファイル

※ 公開されていないので以下は筆者が適当に作ったものです

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は完全に無駄実装しちゃいました笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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本当に多いので一回ハッカソンに出ると人生変わるかもよ。
と書いておきます。

では。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Python]配列の要素を更新

1次元

for a in hairetsu1:
a = 新しい値

2次元

for a in hairetsu2:
a['要素'] = 新しい値

置換するとき
a['要素'] = a['要素'].replace('置換前','置換後')

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Numpy (n,)と(n,1)表記の違いについて【横ベクトルか縦ベクトルかの違い】

はじめに

Numpyのndarray配列のshapeを見ると、(n,1)や(n,)といった表記を目にします。「1次元配列なら【,】も【1】も不要なのでは?」と思って調べてみました。

結論

(n,1)は横ベクトルの配列(ただの1次元配列)

(n,)は縦ベクトルの配列

コメント 2019-12-19 191200.png
※1次元の配列は.Tでは転置に変換することができないので、reshapeを用いる必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google SpreadSheetから設定値を読み取るgssetting

aratana Advent Calendar 2019の20日目です。
aratana

最近、設定ファイルにサイト情報を書いて、チェックしにいくというスクリプトを動作させているのですが、チェック対象サイトが増えるたびに、設定ファイルを自分で書き換えるという作業が発生しているので、改善したいなと思ってきました。

現状だいたい下記のような流れです。

マネージャーががんばる -> サイトオープン -> サイトオープンしたよ連絡
 -> シートにサイト情報を記入
 -> 私に連絡が来る -> 私がサーバに入って、設定ファイルに反映

私の作業は数分ですが、めんどうだなーって思ってきました。
スプレッドシートに書いてあるサイト情報をマスタにして、スクリプトから取得すれば解決だな!と思ったので、スプレッドシートから設定値を楽に取得するパッケージを作りました。Pythonで。

gssetting

安直ですが、Google SpreadSheet Settingの略で、gssettingと命名してみました。
設定情報のモデルを作成し、それを元にシートから情報を取ってくるイメージです。
gspread依存です。

2019/12/20現在、急ぎで作ったのでアルファ扱いで、完成度は低いですが、今後改善していきます。(テストファイルとかつくらなきゃ

利用方法

はじめに

まず、サービスアカウントを作成して、対象のシートに設定してください。
下記サイトをご参考に。。。
PythonでGoogleスプレッドシートを編集

インストール

PyPiに登録しているので、ピップ!します。(現在は、Python3.7以上対象となってます。

pip install gssetting

モデル作成

下記シートから、name列とvalue列のみを抽出しようかと思います。

スクリーンショット 2019-12-19 16.48.28.png

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 '漢字'>)]
私
僕

まとめ

楽に、シートから設定を取得できるパッケージは完成したので、色々なところで使いまわししていこうかなと思ってます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

手認識が実用レベルに到達した件

MediaPipe(Google)のHandTrackingで、Issueで話題になっているジェスチャー実装について書きます。

こんな感じで動きました

百聞は一見にしかずということで先に検証結果です。
out.gif

MediaPipe

今年6月のCVPR2019でMediaPipeの発表があり、
そのアプリケーションのひとつとしてHandTrackingが公開されました。
MediaPipeはMLパイプラインと呼ばれるコードを書かなくても
ビジュアライズツールだけでMLアプリケーションを構築するフレームワークです。

image.png

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

手認識は何処で使えるか?

r-vte.gif

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.py

iOSでジェスチャー導入したかったのですが今のところAPIから回転が取得できないようです。
Access hand landmarks position in iOS #237

おわりに

今回は、ジェスチャー実装を試しました。
ついにか、ようやくかHandTrackingが実用レベルに到達しました。
来年(2020年)は様々な活用が出てきそうですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の種類

いくつかありますが、主なカテゴリの RDBNoSQL の2種類を紹介します。

RDB

関係データベース(Relational DataBase) の略で、MySQLPostgreSQL など、
世の中で使われている DB や解説されている DB の多くは RDB です。
初学者にとっては、SQLの学習コストやテーブル設計の難しさが壁になります。

Heroku では PostgreSQL が公式 Add-on で提供されています。
Postgres - SQL データベース・サービス | Heroku

NoSQL

NoSQL は(No SQLではなく) Not only SQL の略で、
SQL を使わずに、Pythonの文字列やリストなどを扱うような感覚で、
シンプルにデータを管理できるという魅力があります。

NoSQL というカテゴリの中にも様々な種類があり、
例えば MongoDBドキュメント型 で、Neo4jグラフ型 です。
今回使う Redisキーバリュー型(Key-Value-Store : KVS) です。

Redis

オンメモリで読み書きが高速、永続化も可能な KVS です。
主にキャッシュのために使われることが多いですが、
最近ではランキングデータに使われるという話もよく見かけます。

String List Set Hash SortedSet などの
様々なデータ型をサポートしています。
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 planHobby 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.0redis-py 3.3.11 で動作確認しています。

参考リンク(クリックで開く)

redis-py のインストール

簡単に操作を行うためのライブラリを導入します。

ローカルで試す場合は以下をターミナル(コマンドプロンプト)で実行してください。

python3 -m pip install --upgrade pip # 念の為実行しておく
python3 -m pip install redis # redis-pyのインストール

Heroku 上で実行する場合は requirements.txtredis を追加する必要があります。

requirements.txt
redis==3.3.11

Redisに接続する

ここで環境変数 REDIS_URL を使います。
Heroku上で実行する場合はそのままで大丈夫ですが、
ローカルで実行する場合は設定しておくか、redis://~ を直接書いてください。

まず接続用のモジュールを用意しておきます。

r.py
import redis
import os

def connect():
    return redis.from_url(
        url=os.environ.get('REDIS_URL'), # 環境変数にあるURLを渡す
        decode_responses=True, # 日本語の文字化け対策のため必須
    )

以下、これをPythonインタプリタで使っていきます。
※ コマンドプロンプトに python3py -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('タイトル')
1

exists メソッドの引数には複数の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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NoSQL×PaaSで運用するナレッジベース+WebAPI

これはなに

NoSQLとPaaSでナレッジベース+WebAPIを構築した際のノウハウのまとめです。

文字列タグ指向無向グラフ型ナレッジベース というアーキテクチャを実装する場合に、
Heroku + Redis + FastAPI で構築した例と、
AWS(DynamoDB + Lambda + API Gateway) で構築した例を紹介します。

コード部分は全て Python3.8.0 を使用しています。

※ 現在AWS編が未完成です。ごめんなさい。

ナレッジベースとは?

ナレッジベースには様々な定義がありますが、この記事では
「知識をコンピュータが読み取り可能な形式で格納したデータベース」を指します。
知識ベース ナレッジデータベース KB などの呼ばれ方もあります。

参考リンク(クリックで開く)

文字列タグ指向無向グラフ型ナレッジベース

今回例として構築するナレッジベースです。
名前だけでは分かりにくいと思うので、イメージ図を用意しました。
(可視化は未実装のためマインドマップツール coggle で作成)

スクリーンショット 2019-12-03 20.00.32.png

なお、このアーキテクチャは流通しているものではなく、
GraphQLから着想を得て独自に設計したものです。
(サーベイはしていないのでもしかしたら既存かもしれません)

「文字列タグ指向」について

このナレッジベースでは文字列(とその集合)データのみを扱い、
全ての文字列をタグとして扱います。

上の図の例では、
Webサービス名 アカウントID URL 記事タイトル 概念 プログラミング言語
などの各文字列を1つのタグとして扱っています。

仕様上、文字列には空白や改行文字などは含まないものとします。

「無向グラフ型」について

このナレッジベースでは、関係のあるタグを結び付けるようにします。

例えば フレームワーク というタグには
Rails Laravel Django Flask
というタグが紐付いているというデータが取得可能で、

スクリーンショット 2019-12-03 20.43.44.png

例えば QiitaPython のどちらのタグも紐付いているのは
https://qiita.com/1ntegrale9/items/94ec4437f763aa623965
というタグ(QiitaのPythonに関する記事URL)、のようにデータを取得できます。

スクリーンショット 2019-12-03 20.44.33.png

上の図では頂点(文字列)がタグ、辺が関係を表しています。
そして無向なので双方参照が可能です。
また包含関係などは考慮しないので、重み付けはありません。

参考記事:グラフ理論の基礎 - Qiita

このナレッジベースの役割

「2つの文字列を格納する」という単純な操作の繰り返しだけで、
辞書的な知識体系(集合知)を形成することです。

そして爆速で育て上げるためにWebAPIが必要になります。

構築例:Redis + FastAPI + Heroku

手軽に無料で運用したい場合はこちらを採用します。

Heroku の初期設定や Redis の基本操作はこちらで解説しています。
Heroku×Redis×Python で始める NoSQL DB 入門 - Qiita

Redis

オンメモリで読み書きが高速なKVSです。永続化にも対応しています。
1つのタグに複数のタグを紐付けたいので、集合型のみを使います。

ライブラリのインストール

Python で扱うため、redis-py を利用します。

python3 -m pip install redis hiredis

hiredis-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 Python

FastAPI

FastAPI は Python の Web フレームワークの1つで、
シンプルな WebAPI を少ないコードで実装できるのと、
API ドキュメントを設定なしで自動生成してくれるのが特徴です。

Flask Responder Starlette DRF などはオーバースペックで、
Bottle は逆に機能が足りず、FastAPIがちょうどよくハマりました。

ライブラリのインストール

python3 -m pip install fastapi uvicorn email-validator

Uvicorn は高速なASGIサーバーです。FastAPIの起動に使います。
Gunicorn の typo ではないです。

email-validator は入れておかないと起動時に怒られます。何故か。

アプリケーションの初期化

非常にシンプルです。

main.py
from fastapi import FastAPI
app = FastAPI()

引数の titlediscription を設定すると、
上記画像のような自動生成される API Doc にタイトルと説明部が反映されます。

main.py
app = FastAPI(
    title='collective-intelligence',
    description='文字列タグ指向無向グラフ型ナレッジベース',
)

また、docs_url を指定すると、API Doc のURLを変更できます。
デフォルトでは /docs ですが、ルートにしておくのも良いでしょう。

main.py
app = 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.py
from 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 も一定枠まで無料でホスティングしてくれます。

最初に以下の手順が必要です。

必要なファイルの用意

以下のファイルが必要になります。
これを GitHub リポジトリに用意します。

$ tree
.
├── main.py          # アプリケーション
├── Procfile         # プロセス実行コマンド定義ファイル
├── requirements.txt # 依存ライブラリ定義ファイル
└── runtime.txt      # Pythonバージョン定義ファイル
Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT
requirements.txt
fastapi
email-validator
uvicorn
redis
hiredis
runtime.txt
python-3.8.0

実際のディレクトリ も参考にしてください。

アプリケーションのデプロイ

Dashboard の Deploy タブからデプロイ作業を行います。
GitHub と連携してリポジトリを紐付け、Manual Deploy を実行します。
Automatic deploys も設定しておくと、master への push 時に自動でデプロイしてくれます。

スクリーンショット 2019-12-19 14.45.06.png

build が無事に完了したら、
登録されたプロセスを Configure Dynos から ON にしておきます。

スクリーンショット 2019-12-19 14.48.21.png

Dashboard 右上の Open app からデプロイされたアプリケーションを確認できます。

構築例:AWS(DynamoDB + Lambda + API Gateway)

執筆中のため公開をお待ちください

スケーラビリティを意識するならこちらを採用します。
柔軟にデータ構造を変えることも可能です。

初めての、LambdaとDynamoDBを使ったAPI開発 - Qiita
API Gateway + Lambda + DynamoDB - Qiita

Amazon DynamoDB

RDBと同様に1テーブル1プライマリキーが基本です。
プライマリキーはデータを一意に識別するためのキーで、「パーティションキー」または
「パーティションキーとソートキーの複合キー」のことです。
ソートキーを追加することによってパーティションキーのユニーク制限を緩和できます。

開始方法 - Amazon DynamoDB | AWS
初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~ | Developers.IO
無料枠で頑張るためにDynamoDBのキャパシティを理解する - ITと筋トレの二刀流

テーブルの設計

パーティションキー:タグ
ソートキー:timestamp

テーブルの作成

AWS Lambda

初めてのサーバーレスアプリケーション開発 ~LambdaでDynamoDBの値を取得する~ | Developers.IO
GitHub Actionsを使ってAWS Lambdaへ自動デプロイ (詳説+デモ手順付きver) - Qiita

2つのタグを紐付けて格納する

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を試してみた - エムティーアイ エンジニアブログ

メソッドの作成

create-method.png

メソッドの選択

create-post.png

メソッド管理画面

do-pull.png

PULLモデルの作成

model-pull.png

PUSHモデルの作成

model-push.png

リクエストの検証を設定

request-pull.png

PULL API のテスト

test-pull.png

PUSH API のテスト

test-push.png

利用料金に関して

Billing 画面の請求書から確認します。

まだ本稼働しているわけではないですが、
テストで数百件のリクエスト/レスポンスの送受信を行った結果0円だったので、
お試しで使う分には恐れることはなさそうです。

スクリーンショット 2019-12-05 19.39.49.png

GCP vs AWS

GCP(Firestore)とAWS(DynamoDB)で悩みましたがDynamoDBを採用しました。

GCP側で選択する場合、4つのデータストアサービスから悩むことになると思いますが、
趣味で使うならFirestore一択だと思います。
データベースを選択: Cloud Firestore または Realtime Database  |  Firebase

終わりに

これらはほぼ独学で得た内容ですが、
新しい技術を習得するスキルは会社のモダンな環境で得られたものだと思います。
強いエンジニアが新技術をガンガン使っていく現場で働けるというのは最高の体験です。

また、Heroku側の構成のものを公開しています。
公開時点でデータは空ですが、自由に触ってみてください。
https://collective-intelligence.herokuapp.com/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 

null_hantei.jpg

'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 データセットを可視化してみます.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 list

pipはパッケージ管理で利用されている。
主として行えるのはパッケージの、
・インストール
・アップグレード
・リムーブ
そのほか様々なコマンドがあり、以下表に一部をまとめてみる。

subcommand            意味
install パッケージを指定してインストール可能
--updateで最新バージョンをインストール可能
uninstall installの逆
freeze インストールされたパッケージのリスト表示(install向け表示)
list 仮想環境にインストールされたすべてのパッケージを表示
show show パッケージ名でパッケージ情報を表示
問2:以下のプログラムを実行した際の出力結果を選びなさい。

正解:0 1 2

num_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:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。

正解:Noro

def 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:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。

正解: xxxxCODExxxxDIVExxxxDIVE

replace.py
d = 'xxxxDIVExxxxDIVExxxxDIVE'

print(d.replace('DIVE', 'CODE', 1))

文字列.replace(置換前の文字列, 置換後の文字列, 最大回数)

問6:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。

正解:3.142

%全体の幅.小数点以下の幅f

format.py
print("出力結果:")
print('円周率は%5.3fである。'%math.pi)

円周率は3.142
問7:以下のプログラムを実行した際の出力結果として正しいものを選択しなさい。

正解:[('Noro', 'Kimura', 'Matsumoto'), ('Nakao', 'Miyashita', 'Tanaka'), ('Miyaoka', 'Shibata', 'Ivan')]

zipを用いてリスト型:listを辞書型:dictionaryにする方法

zip.py
dic = [
    ['Noro', 'Nakao', 'Miyaoka'],
    ['Kimura', 'Miyashita', 'Shibata'],
    ['Matsumoto', 'Tanaka', 'Ivan'],
]

print(list(zip(*dic)))

以下を参考にしました。
https://pg-chain.com/python-dictionary-list

以上です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.pdf

Proposal 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-selections

Regular 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が経路候補の以下のログの場合,以下の赤枠のように計算できます。

CO2_calc.png

また,クリックログからこのユーザーが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などのトップカンファレンスに挑戦していきたいと思います。

それではみなさん,メリークリスマス&よいお年を!


  1. 世界最高峰のデータ分析競技会「KDD CUP 2019」で世界1位を受賞 https://www.nttdocomo.co.jp/binary/pdf/info/news_release/topics_190809_01.pdf 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データサイエンス学ぼう

Python

(未記載)

機械学習(入門)

  1. モデルの仕組み
  2. データ分析入門
  3. 機械学習の初モデルを作りましょう。
  4. モデルを検証しましょう(Model validation)
  5. Underfitting and Overfitting
  6. Random Forest
  7. まとめて練習宿題

機械学習(中級)

(未記載)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ:[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'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.py
font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
                    size=np.floor(8e-3 * image.size[1] + 0.5).astype('int32'))

解決策3 プログラムの変更

今回検出画像ではなく、クラス名とその座標が得られればいいので描写に関する部分は消すことで対応。

yolo_video.py
def 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円程度も上がってしまったため、インスタンスを削除。

検出されない原因の解明と最適なインスタンスタイプの検討をしてからまた取り組みます。
エラーの心あたりある方いたらコメント欲しいです!お願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む