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

【Tensorflow-gpu 1.x系】複数のモデルを一つのプログラムで実行する

はじめに

以下のように複数のモデル(手検出モデルと指先検出モデル)を1つのプログラムで実行しようとした時にGPUメモリで躓いたためメモ。
以下のプログラムでは手検出で1つ、指先検出で1つの計2つのモデルを利用しています。

躓いたポイント

1つ目のモデルを読み込んだ後、2つ目のモデルを動作させようとするとGPUメモリ不足でプログラムが異常終了する。

原因

TensorFlowはデフォルト設定では、GPUのメモリのほぼ全てを使用して、メモリの断片化を軽減するようにしているようです。
1つ目のモデルの時点でメモリをほぼ全て使い切ったため、2つ目のモデルは動かせなかった模様。

対処

Allowing GPU memory growthオプションを利用し、必要なメモリだけ確保するようにする。
※ただし、プログラム途中でGPUメモリ解放等はしないように注意が必要とのこと

ソースコード

以下のような指定をする。

    config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
    sess = tf.Session(graph=net_graph, config=config)


ソースコード全体のイメージ ※イメージです
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import copy
import cv2 as cv
import tensorflow as tf
import numpy as np


def session_run(sess, inp):
    out = sess.run([
        sess.graph.get_tensor_by_name('num_detections:0'),
        sess.graph.get_tensor_by_name('detection_scores:0'),
        sess.graph.get_tensor_by_name('detection_boxes:0'),
        sess.graph.get_tensor_by_name('detection_classes:0')
    ],
    feed_dict={
        'image_tensor:0':
        inp.reshape(1, inp.shape[0], inp.shape[1], 3)
    })
    return out


def main():
    print("Hand Detection Start...\n")

    # カメラ準備 ##############################################################
    cap = cv.VideoCapture(0)
    cap.set(cv.CAP_PROP_FRAME_WIDTH, 1280)
    cap.set(cv.CAP_PROP_FRAME_HEIGHT, 720)

    # GPUメモリを必要な分だけ確保するよう設定
    config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))

    # 手検出モデルロード ######################################################
    with tf.Graph().as_default() as net1_graph:
        graph_data = tf.gfile.FastGFile('frozen_inference_graph1.pb',
                                        'rb').read()
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(graph_data)
        tf.import_graph_def(graph_def, name='')

    sess1 = tf.Session(graph=net1_graph, config=config)
    sess1.graph.as_default()

    # 指先検出モデルロード ######################################################
    with tf.Graph().as_default() as net2_graph:
        graph_data = tf.gfile.FastGFile('frozen_inference_graph2.pb',
                                        'rb').read()
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(graph_data)
        tf.import_graph_def(graph_def, name='')

    sess2 = tf.Session(graph=net2_graph, config=config)
    sess2.graph.as_default()

    while True:
        start_time = time.time()

        # カメラキャプチャ ####################################################
        ret, frame = cap.read()
        if not ret:
            continue
        debug_image = copy.deepcopy(frame)

        # 手検出実施 ##########################################################
        inp = cv.resize(frame, (512, 512))
        inp = inp[:, :, [2, 1, 0]]  # BGR2RGB

        out = session_run(sess1, inp)

        rows = frame.shape[0]
        cols = frame.shape[1]

        num_detections = int(out[0][0])
        for i in range(num_detections):
            class_id = int(out[3][0][i])
            score = float(out[1][0][i])
            bbox = [float(v) for v in out[2][0][i]]

            if score < 0.8:
                continue

            x = int(bbox[1] * cols)
            y = int(bbox[0] * rows)
            right = int(bbox[3] * cols)
            bottom = int(bbox[2] * rows)

            # 指先検出実施 ####################################################
            if class_id == 3:
                trimming_image = debug_image[y:bottom, x:right]
                inp = cv.resize(trimming_image, (300, 300))
                inp = inp[:, :, [2, 1, 0]]  # BGR2RGB

                f_rows = trimming_image.shape[0]
                f_cols = trimming_image.shape[1]

                out = session_run(sess2, inp)

                f_num_detections = int(out[0][0])
                for i in range(f_num_detections):
                    f_score = float(out[1][0][i])
                    f_bbox = [float(v) for v in out[2][0][i]]

                    f_x = int(f_bbox[1] * f_cols)
                    f_y = int(f_bbox[0] * f_rows)
                    f_right = int(f_bbox[3] * f_cols)
                    f_bottom = int(f_bbox[2] * f_rows)

                    if f_score < 0.4:
                        continue

                    # 指検出結果可視化 ########################################
                    cv.rectangle(
                        trimming_image, (f_x, f_y), (f_right, f_bottom),
                        (0, 255, 0),
                        thickness=2)

            # 手検出結果可視化 ################################################
            cv.rectangle(
                debug_image, (x, y), (right, bottom), (0, 255, 0), thickness=2)

        # 処理時間描画 ########################################################
        elapsed_time = time.time() - start_time
        time_string = u"elapsed time:" + '{:.3g}'.format(elapsed_time)
        cv.putText(debug_image, time_string, (10, 50), cv.FONT_HERSHEY_COMPLEX,
                   1.0, (0, 255, 0))

        # 画面反映 ############################################################
        cv.imshow(' ', debug_image)
        cv.moveWindow(' ', 100, 100)

        key = cv.waitKey(1)
        if key == 27:  # ESC
            break


if __name__ == '__main__':
    main()

以上。

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

WebElement 内部を find_element_by_xpath() で再検索した時のつまずき

python selenium の WebDriver でブラウザテストを作成しているときにWebElement 内部 の検索にfind_element_by_xpath を使った際につまずきましたのでまとめます。

つまずき1:要素内部についての find_element_by_xpath() 検索のはずなのに、別の要素の値が取れてしまう

browserTest.py
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.chrome.webdriver import WebDriver

def printUserEmails(driver:WebDriver)->None:
    UserTables = driver.find_elements_by_xpath('//table[start-with(@name, "user")]')
    if len(UserTables) == 0:
        print('Userが表示されていません。')
        return
    for UserTable in UserTables:
        emailElement = UserTable.find_element_by_xpath('//input[@type="text" and @name="email"]')
        print(emailElement.get_attribute('value'))

結果.txt
user1@aaa.bbb
user1@aaa.bbb
user1@aaa.bbb
user1@aaa.bbb
.
.

■状況
「一度 find_elements_by_xpath() でとった WebElement の List について for ループを回しているにも関わらず一番最初の要素の同一の値が取れてしまう」という現象につまずきました。

■原因:XPath の文字列の最初の「//」が「HTML内の全要素」という意味のため再検索していた

xpath「//」←「HTML内の全要素」から検索するの意味
(こちらの記事に詳細に記載して頂いておりましたので参考にさせて頂きました。
https://www.techscore.com/tech/XML/XPath/XPath3/xpath03.html/#xpath3-2

element.find_element_by_xpath('//tag名') #  → Rootから検索するので同じデータが取得されてしまう
element.find_element_by_xpath('tag名') # → 対象要素から検索できる

■対応:xpath の文字列 から最初の「//」を消す

browserTest.py
#ーー中略ーー
    for UserTable in UserTables:
        emailElement = UserTable.find_element_by_xpath('input[@type="text" and @name="email"]')
        print(emailElement.get_attribute('value'))


これでルートHTMLから検索されることはなくなりました。

つまずき2:要素からEmailが見つからなくなってしまった。

上記の対応で、「//」を消して、root からの検索をしないようにしたのですが、今まで取得できていた、検索がかからなくなり失敗するようになってしまいました。

■原因:「//」が特殊で、通常XPathは全パスを指定しないといけないため。

通常XPath はHTML からの全要素を指定して記述することで、DOMを解析し要素を特定するようで(「//」が特殊で全要素を読み込むという意味
User の table タグ直下に input が存在しないため、検索に失敗しまいました。

element.find_element_by_xpath('tag名') 
# → 対象要素直下に検索対象のtagがないと検索失敗となる。

■対応:子要素 の 内部 全検索は「.//検索条件」

element.find_element_by_xpath('.//tag名') 
# → 対象要素直下に検索対象のタグがあれば取得する

要素.find_element_by_xpath('.//検索条件') で記述するようにすることで、current node からの検索として、全検索できるようでした。
https://stackoverflow.com/questions/21578839/how-to-find-the-element-within-element-in-selenium

browserTest.py
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.chrome.webdriver import WebDriver

def printUserEmails(driver:WebDriver)->None:
    UserTables = driver.find_elements_by_xpath('//table[start-with(@name, "user")]')
    if len(UserTables) == 0:
        print('Userが表示されていません。')
        return
    for UserTable in UserTables:
        emailElement = UserTable.find_element_by_xpath('.//input[@type="text" and @name="email"]')
        print(emailElement.get_attribute('value'))

結果.txt
user1@aaa.bbb
userDummy@aaa.bbb
aaa@aasdfaa.bbb
e1@aaa.bbb
.
.


以上です。

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

Pythonってなんだろう(随時更新)

Pythonって何? Python_logo.png

Python = 汎用プログラミング言語1、スクリプト言語2、Google三大言語3の一柱
- AI、ML(機械学習)、DL(深層学習)に特に向いている(ライブラリが揃っている)
- 本格的なWebサービスが作れる
- ライブラリが豊富
- 可読性に優れている
- コードの書き方が個人に依存しない

こんなところにPythonが!!

PythonはGoogleが使用する主要言語になっているように、さまざまなWebサービス/アプリケーションに用いられています。
事例(私が使っている範疇のもので)

  • Anki
  • シヴィライゼーション4
  • World of Tanks
  • Instagram
  • Pinterest
  • Google App Engine
  • Pepper

特に科学関連のライブラリが豊富

数学ライブラリとして Matplotlib, Numpy, SymPy, Sage
科学ライブラリとして Scipy, scikit-learn, Mlpy
などがある。


  1. 特定の用途に向けて作られた言語ではなく、何でもできる言語 

  2. 簡易な記述ができるプログラミング言語、その分実行速度はコンパイル言語に比べると遅くなる 

  3. Google三大言語の残りの2つはC++とJava 

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

SEIYU風のECサイトを作りましょう(2)Xadminを使って管理画面を一新します

前回の記事

SEIYU風のECサイトを作りましょう(1)要求分析とプロジェクト初期化

Xadminとは何か

GitHub上に公開されてるDjangoの管理画面の上位互換パッケージ。
キャプチャ.PNG

管理画面本来の機能を拡張し、見た目も良くします。
https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_320164_3cdce46d-cb8b-9fc9-3b6d-c38e8e618403.jpg
前回の記事で完成した管理画面を、Xadmin使用してリメイクすれば、以下のようになります。
キ21ャプチャ.PNG
また、スタイルの切り替えも可能ですし、データのexcel出力などもデフォルト機能としてはいってます。
キャプチャ.PNG

では、早速作っていきます。

Xadminダウンロード

githubのリポジトリはこちらになります。xadmin

  • ブランチはdjango2を選んでダウンロードしてください
  • あるいは本記事のリポジトリextra_appsからxadminを抜いて使ってください

本記事のリポジトリから抜いて使うことがオススメです:point_up:

注意事項
pip install xadminは絶対しないことです、古いバージョンのxadminをおとすことで、パッケージ依存がややこしいことになります。

依存パッケージインストール

以下コマンドを実行し、xadminに必要なパッケージをインストールします。

公式参考リンク

pip install django-crispy-forms
pip install django-import-export
pip install django-reversion
pip install django-formtools
pip install future
pip install httplib2
pip install six

ファイル配置

appsと同じディレクトリで、extra_appsという名のフォルダを作ります。
extra_apps配下にダウンロードしたxadminを置きます。
完成後のディレクトリ構成は以下になります。

qiita-Django-supermarket
|-- requirements.txt
|-- README.md
|-- LICENSE
|-- CHANGELOG.md
|-- api
|-- |--api
|-- |-- |-- __init__.py
|-- |-- |-- settings.py
|-- |-- |-- urls.py
|-- |-- |-- wsgi.py
|-- |--apps
|-- |-- |-- goods
|-- |-- |-- users
|-- |--extra_apps
|-- |-- |-- xadmin
|-- |--manage.py

その後settings.pyに以下の内容を追加します。

settings.py
...
sys.path.insert(0, os.path.join(BASE_DIR, 'extra_apps'))
...

INSTALLED_APPS = [
    ...
    'crispy_forms',
    'xadmin',
]

その後extra_appsappsと同じようにsouruces Rootに指定します。
PyCharmの操作方法は
1.extra_appsを右クリック
2. Mark Directory as
3. souruces Root

usersとgoods appにadminx.pyファイルを作る

usersアプリに移動して、adminx.pyファイルを作ります

cd apps/users
vim adminx.py
apps/users/adminx.py
import xadmin
from xadmin import views
from .models import VerifyCode


class BaseSetting(object):
    enable_themes = True
    use_bootswatch = True


class GlobalSettings(object):
    site_title = "ネットスーパー"
    site_footer = "supermarket_Back"


class VerifyCodeAdmin(object):
    list_display = ['code', 'mobile', "add_time"]


xadmin.site.register(VerifyCode, VerifyCodeAdmin)
xadmin.site.register(views.BaseAdminView, BaseSetting)
xadmin.site.register(views.CommAdminView, GlobalSettings)

その後goodsファイルに移動し、adminx.pyを作ります

cd ../goods
vim adminx.py
apps/goods/adminx.py
import xadmin
from .models import Goods, GoodsCategory, GoodsImage, GoodsCategoryBrand, Banner, HotSearchWords
from .models import IndexAd

class GoodsAdmin(object):
    list_display = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
                    "shop_price", "goods_brief", "is_new", "is_hot", "add_time"]
    search_fields = ['name', ]
    list_editable = ["is_hot", ]
    list_filter = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
                   "shop_price", "is_new", "is_hot", "add_time", "category__name"]

    class GoodsImagesInline(object):
        model = GoodsImage
        exclude = ["add_time"]
        extra = 1
        style = 'tab'

    inlines = [GoodsImagesInline]


class GoodsCategoryAdmin(object):
    list_display = ["name", "category_type", "parent_category", "add_time"]
    list_filter = ["category_type", "parent_category", "name"]
    search_fields = ['name', ]


class GoodsBrandAdmin(object):
    list_display = ["category", "image", "name", "desc"]

    def get_context(self):
        context = super(GoodsBrandAdmin, self).get_context()
        if 'form' in context:
            context['form'].fields['category'].queryset = GoodsCategory.objects.filter(category_type=1)
        return context


class BannerGoodsAdmin(object):
    list_display = ["goods", "image", "index"]


class HotSearchAdmin(object):
    list_display = ["keywords", "index", "add_time"]


class IndexAdAdmin(object):
    list_display = ["category", "goods"]


xadmin.site.register(Goods, GoodsAdmin)
xadmin.site.register(GoodsCategory, GoodsCategoryAdmin)
xadmin.site.register(Banner, BannerGoodsAdmin)
xadmin.site.register(GoodsCategoryBrand, GoodsBrandAdmin)

xadmin.site.register(HotSearchWords, HotSearchAdmin)
xadmin.site.register(IndexAd, IndexAdAdmin)

urls.pyにxadmin用のpathを追加

api/urls.py
import xadmin

urlpatterns = [
    ...
    path('xadmin/', xadmin.site.urls),
    ...
]

最後にマイグレートを実行します。
xadmin内部にmigrateファイルがあるため、それを実行します。

python manage.py makemigrations 
python manage.py migrate

ここまで来れば

python manage.py runserver

サーバーを立ち上げてhttp://127.0.0.1:8000/xadminにアクセスすると、xadminにアクセスできるようになってるはずです。

補足

アプリの表示名を修正したい場合。
キャプチャ.PNG

下記の二つの手順を実行してください。

apps/goods/apps.py
from django.apps import AppConfig


class GoodsConfig(AppConfig):
    name = 'goods'
    verbose_name = "商品だよ"

api/setting.py
INSTALLED_APPS = [
...
 'goods.apps.GoodsConfig'
...
]

修正後、以下のようになります。
キャプチャ.PNG

次回予告

思ったよりxadminを紹介するのに時間がかかりました:frowning2:
次回は一気にAPIを作って行きたいと思います。
そしていよいよVue.jsのセットアップです。

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

OpenAI Gymの環境 改造方法メモ

OpenAI Gym1とは、OpenAIが提供している強化学習のシミュレーション用プラットフォームであり、強化学習アルゴリズムの開発・比較に利用されています。
基本的な使い方については類似記事が多くあり、お試しで動かしてみる際に非常に参考になります。一方で、色々触ってみると「報酬設計を少し変えて動かしたい」といった感じで、環境に手を加えたい欲が出てきます。ところが、このように環境を改造して動かすにはどうすれば良いのかについてはあまり書かれていないようです。
そこで、自分用にOpenAI Gymの環境の改造方法をメモ書きしてみました。自分でソースコード2を見ながらメモ書きしたため、ベストプラクティスとは言えない方法もあるかもしれません。気づいたことがあった場合、随時記事を更新する予定です。

OpenAI Gymの基本的な使い方

OpenAI Gymはpipでインストールすることができます。

pip install gym

gymには、強化学習の代表的な環境がいくつか用意されています。
そのうちの一つ、CartPole-v0(カート上のポールの直立状態を維持するゲーム)呼び出したあと、ランダムに行動して動かしてみましょう。

import gym
from gym.spaces.prng import seed as space_seed


env = gym.make('CartPole-v0') #環境の呼び出し
#擬似乱数のシードを設定
env.seed(0) #環境の初期化に使用される擬似乱数のシード
space_seed(0) #行動に使用される擬似乱数のシード

obs = env.reset() #環境を初期化する
terminal = False #環境が終端かどうかの判定
n_steps = 0
while not terminal:
    action = env.action_space.sample()  #行動をランダムに選択する
    obs, reward, terminal, info = env.step(action) #状態を遷移させる

    n_steps += 1
    print(f'step:{n_steps} reward:{reward} terminal:{terminal}')


実行結果
step:1 reward:1.0 terminal:False
step:2 reward:1.0 terminal:False
step:3 reward:1.0 terminal:False
step:4 reward:1.0 terminal:False
step:5 reward:1.0 terminal:False
step:6 reward:1.0 terminal:False
step:7 reward:1.0 terminal:False
step:8 reward:1.0 terminal:False
step:9 reward:1.0 terminal:False
step:10 reward:1.0 terminal:False
step:11 reward:1.0 terminal:False
step:12 reward:1.0 terminal:False
step:13 reward:1.0 terminal:False
step:14 reward:1.0 terminal:True

環境を呼び出し、env.reset()で初期化して、env.step(action)で行動を遷移させるのがgymの基本的な使い方です。
強化学習のアルゴリズムを実装するときには、actionを方策に基づいて選択する処理と、env.step(action)の戻り値rewardをもとに価値や方策を学習する処理を記述することになります。

※ソースコード中のenv.stepの戻り値infoは、デバッグ用の情報をdictに格納した変数です。(正直、使い方がよく分かっていません。)

※終端を表す変数の名前にはdoneを使うのが慣例ですが、doneでは何を表す変数なのかが分かりにくいと思っているのでterminalと命名しています。

環境の改造

gymには、環境インスタンスを内包し、resetやstepの振る舞いの変更を行うためのラッパークラスgym.Wrapperが用意されています。gym.Wrapperを継承したクラスを自作することで環境の改造が可能になります。
環境の改造を実現する自作クラスは、次のように実装します。

class MyRemodeledEnv(gym.Wrapper):
    def __init__(self, env):
        super().__init__(env) #親クラスの呼び出しを忘れないこと

    def reset(self, **kwargs):
        obs = self.env.reset(**kwargs)
        """
        ここにresetの振る舞いを記述
        """
        return obs #振る舞い変更によって書き換えられた観測を返す

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)
        """
        ここにstepの振る舞いを記述
        """
        return obs, reward, terminal, info #振る舞い変更によって書き換えられた観測, 報酬, 終端判定, デバッグ情報を返す

実装した自作クラスは、次のように呼び出します。

env = gym.make('CartPole-v0')
env = MyRemodeledEnv(env)

このとき、ラッパーを外からはgym.Envと同じようなクラスに見えるため、両者の違いを意識せずに使うことができます。
また、複数のラッパークラスを実装し、シーケンシャルに呼び出すことも可能です。

class MyRemodeledEnv1(gym.Wrapper):
    """
    ラッパー1の改造の記述
    """

class MyRemodeledEnv2(gym.Wrapper):
    """
    ラッパー2の改造の記述
    """

env = gym.make('CartPole-v0')
env = MyRemodeledEnv1(env)
env = MyRemodeledEnv2(env)

最終的なenvは、2つのラッパーの改造が両方反映されたものになります。これにより、複数の改造を施すとき、それぞれを別のクラスに分けて実装することが可能です。

改造例

前章を踏まえて、以下環境改造の具体例をメモ書きしていきます。

改造例1 状態遷移のスキップ

通常の環境では、1回の行動で1ステップ分状態遷移が行われます。これを、1回行動したら2ステップ分同じ行動を続けるように改造します。

※状態遷移をスキップする考え方は、深層強化学習のベースとなった論文にて、学習の工夫の一つとして取り入れられているものです。

class SkippingEnv(gym.Wrapper):
    def __init__(self, env, n_skips=2):
        assert n_skips >= 1
        super().__init__(env)
        self.n_skips = n_skips #スキップ数

    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        #n_skips分同じ行動を続ける
        for _ in range(self.n_skips):
            obs, reward, terminal, info = self.env.step(action)
            if terminal:
                break
        return obs, reward, terminal, info


動かしてみたときの実装と結果

実装
env = gym.make('CartPole-v0') #オリジナルの環境の呼び出し
env = SkippingEnv(env) #実装したラッパーの呼び出し
env.seed(0)
space_seed(0)

obs = env.reset()
terminal = False
n_steps = 0
while not terminal:
    action = env.action_space.sample()
    obs, reward, terminal, info = env.step(action)

    n_steps += 1
    print(f'step:{n_steps} reward:{reward} terminal:{terminal}')

結果

step:1 reward:1.0 terminal:False
step:2 reward:1.0 terminal:False
step:3 reward:1.0 terminal:False
step:4 reward:1.0 terminal:False
step:5 reward:1.0 terminal:False
step:6 reward:1.0 terminal:False
step:7 reward:1.0 terminal:False
step:8 reward:1.0 terminal:False
step:9 reward:1.0 terminal:True

オリジナルの環境では全部では終端まで18ステップを要していますが、SkippingEnvによって2ステップ間続けて同じ行動が与えられるため、改造した環境では半分の9ステップして要していないように見えます。

SkippingEnvを呼び出すときのキーワード引数n_skipsによって、スキップ数を指定できるように実装しました。
このように、オリジナルの環境にない変数を持たせて改造に利用することも可能です。

改造例2 報酬のリスケーリング

CartPole-v0では常に1.0の報酬が与えられますが、この報酬の大きさを0.1に変更します。
例1のようにgym.Wrapperを継承してもいいのですが、gymには報酬を変更するためのラッパークラスgym.RewardWrapperが用意されており、これを使うことにします。
gym.RewardWrapperを継承する場合、rewardメソッドを実装し、そこに報酬の変更を記述することで、step時の報酬を変えることができます。

※この改造には、定常的な報酬として1.0は大きすぎるため、小さくしたいという動機があります。

class RewardRescalingEnv(gym.RewardWrapper):
    def __init__(self, env, c=0.1):
        super().__init__(env)
        self.c = c
        lb_r, ub_r = self.env.reward_range #報酬の下界と上界
        self.reward_range = (lb_r*c, ub_r * c) #self.reward_rangeの上書き

    def reward(self, reward):
        return reward * self.c


動かしてみたときの実装と結果

実装
env = gym.make('CartPole-v0') #オリジナルの環境の呼び出し
env = RewardRescalingEnv(env) #実装したラッパーの呼び出し
env.seed(0)
space_seed(0)

obs = env.reset()
terminal = False
n_steps = 0
while not terminal:
    action = env.action_space.sample()
    obs, reward, terminal, info = env.step(action)

    n_steps += 1
    print(f'step:{n_steps} reward:{reward} terminal:{terminal}')

結果

step:1 reward:0.1 terminal:False
step:2 reward:0.1 terminal:False
step:3 reward:0.1 terminal:False
step:4 reward:0.1 terminal:False
step:5 reward:0.1 terminal:False
step:6 reward:0.1 terminal:False
step:7 reward:0.1 terminal:False
step:8 reward:0.1 terminal:False
step:9 reward:0.1 terminal:False
step:10 reward:0.1 terminal:False
step:11 reward:0.1 terminal:False
step:12 reward:0.1 terminal:False
step:13 reward:0.1 terminal:False
step:14 reward:0.1 terminal:True


CartPole-v0ではreward_rangeが(-inf, inf)になっているためreward_rangeの上書きは不要ですが、reward_rangeが有界な環境にも使えるようにするため、今回の実装では上書きしています。

このように、報酬を少し変えるだけならgym.RewardWrapperを継承するだけで十分です。同様に、gymには状態や行動を変える専用のラッパークラスObservationWrapperやActionWrapperも用意されています。

改造例3 報酬のスパース化

強化学習では、終端以外の状態の報酬を0にするスパースな設計にすることがよくあります。そこで、CartPole-v0の報酬を、ポールが倒れたら-1、それ以外は0になるように改造します。

CartPole-v0の終端判定がTrueになるケースには、次の2つがあります。

1.ゲームオーバーになる(ポールが倒れる、カートが左右に動きすぎる)
2.200ステップ経過する

これらを区別して報酬を設計する必要があるため、例2のようにgym.RewardWrapperを継承するのではなく、gym.Wrapperを継承してstepメソッド内に報酬の計算を記述します。

class SparseRewardEnv(gym.Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)

        x, x_dot, theta, theta_dot = self.unwrapped.state
        #ゲームオーバーかどうかの判定
        failed = x < -self.unwrapped.x_threshold \
                 or x > self.unwrapped.x_threshold \
                 or theta < -self.unwrapped.theta_threshold_radians \
                 or theta > self.unwrapped.theta_threshold_radians
        reward = -1 * int(failed) #rewardの再計算
        return obs, reward, terminal, info


動かしてみたときの実装と結果

実装
env = gym.make('CartPole-v0') #オリジナルの環境の呼び出し
env = SparseRewardEnv(env) #実装したラッパーの呼び出し
env.seed(0)
space_seed(0)

obs = env.reset()
terminal = False
n_steps = 0
while not terminal:
    action = env.action_space.sample()
    obs, reward, terminal, info = env.step(action)

    n_steps += 1
    print(f'step:{n_steps} reward:{reward} terminal:{terminal}')

結果

step:1 reward:0 terminal:False
step:2 reward:0 terminal:False
step:3 reward:0 terminal:False
step:4 reward:0 terminal:False
step:5 reward:0 terminal:False
step:6 reward:0 terminal:False
step:7 reward:0 terminal:False
step:8 reward:0 terminal:False
step:9 reward:0 terminal:False
step:10 reward:0 terminal:False
step:11 reward:0 terminal:False
step:12 reward:0 terminal:False
step:13 reward:0 terminal:False
step:14 reward:-1 terminal:True

self.unwrappedはラッパーが内包している環境インスタンスを取得するプロパティです。self.unwrappedによって、ラッパーから直接envの属性にアクセスし、ゲームオーバーの判定を記述することが可能になります。
この判定には、オリジナルの環境のソースコードにおけるstepメソッド内の(ケース1.の)終端判定の記述を引用しました。

unwrappedを利用すれば、ポールが倒れたと判定させる角度の変更、行動によって与えられるカートへの加速度の変更など、大掛かりな改造が可能になります。

環境例4 カートの座標の報酬化

CartPoleはポールが直立できていればx座標はどこでもよい(=報酬は変わらない)という環境ですが、カートが右にあるほど多くの報酬を与えることにします。
カートのx座標に応じて報酬を与えるPositionRewardEnvと、ゲームオーバーになったら-1の報酬を与えるPenaltyRewardEnvの2つのラッパーを作成してシーケンシャルに呼び出します。

class PositionRewardEnv(gym.Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)
        x, *_ = self.unwrapped.state
        low_x = self.observation_space.low[0]
        high_x = self.observation_space.high[0]
        x_hat = (x - low_x) / (high_x - low_x)
        reward = 0.1 * x_hat
        return obs, reward, terminal, info


class PenaltyRewardEnv(gym.Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)        
        x, _, theta, _ = self.unwrapped.state
        #ゲームオーバーかどうかの判定
        failed = x < -self.unwrapped.x_threshold \
                 or x > self.unwrapped.x_threshold \
                 or theta < -self.unwrapped.theta_threshold_radians \
                 or theta > self.unwrapped.theta_threshold_radians
        if failed:
            reward = -1 #rewardの再計算
        return obs, reward, terminal, info


動かしてみたときの実装と結果

実装

env = gym.make('CartPole-v0') #オリジナルの環境の呼び出し
#ラッパーをシーケンシャルに呼び出す
env = PositionRewardEnv(env)
env = PenaltyRewardEnv(env)
env.seed(0)
space_seed(0)

obs = env.reset()
terminal = False
n_steps = 0
while not terminal:
    action = env.action_space.sample()
    obs, reward, terminal, info = env.step(action)

    n_steps += 1
    print(f'step:{n_steps} reward:{reward} terminal:{terminal}')

結果

step:1 reward:0.04954548738784783 terminal:False
step:2 reward:0.04951449351165423 terminal:False
step:3 reward:0.04952411136201068 terminal:False
step:4 reward:0.04957432375449645 terminal:False
step:5 reward:0.049583832949732204 terminal:False
step:6 reward:0.04963395590817136 terminal:False
step:7 reward:0.049724692913380275 terminal:False
step:8 reward:0.04985606183561966 terminal:False
step:9 reward:0.05002809606219364 terminal:False
step:10 reward:0.05024084065495276 terminal:False
step:11 reward:0.05049434647436899 terminal:False
step:12 reward:0.05078866188204178 terminal:False
step:13 reward:0.05104259318449751 terminal:False
step:14 reward:-1 terminal:True

画像がないので分かりづらいですが、カートが中央の位置の時報酬が0.5になり、終端状態では-1になっています。
つまり、結果から左から右に少しだけ移動してポールが倒れていることが分かります。

強化学習とは、報酬を多く獲得する行動を学習する手法です。
したがって、この環境に強化学習を適用することで、ポールを立たせたまま右に移動してギリギリで止まるような動きを学習できそうです。

学習させてみた

試しに、前章の例4を学習させてみました。学習にはchainerRL3のDouble DQNを使いました。
思ったよりも学習が難しく、少し大掛かりになってしまいました。


実装
import numpy as np
import matplotlib.pyplot as plt
import gym
from gym.spaces.prng import seed as space_seed
import chainer
import chainer.functions as F
import chainerrl
from PIL import Image


class PenaltyRewardEnv(gym.Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)        
        x, _, theta, _ = self.unwrapped.state
        #ゲームオーバーかどうかの判定
        failed = x < -self.unwrapped.x_threshold \
                 or x > self.unwrapped.x_threshold \
                 or theta < -self.unwrapped.theta_threshold_radians \
                 or theta > self.unwrapped.theta_threshold_radians
        if failed:
            reward = -1 #rewardの再計算
        return obs, reward, terminal, info


class PositionRewardEnv(gym.Wrapper):
    def reset(self, **kwargs):
        return self.env.reset(**kwargs) #resetは変更なし

    def step(self, action):
        obs, reward, terminal, info = self.env.step(action)
        x, _, _, _ = self.unwrapped.state
        low_x = self.observation_space.low[0]
        high_x = self.observation_space.high[0]
        x_hat = (x - low_x) / (high_x - low_x)
        reward = 0.1 * x_hat
        return obs, reward, terminal, info


class ObsForChainerRLEnv(gym.ObservationWrapper):
    def observation(self, observation):
        return np.array(observation, dtype=np.float32, copy=False)


def play_episode(env, agent, mode='train', frames=None):
    #mode='train' : 探索あり行動、ネットワークの更新あり
    #mode='eval' : 探索なし行動、ネットワークの更新なし
    assert mode in ['train', 'eval']
    assert frames is None or isinstance(frames, list)
    if mode == 'train':
        explor_and_update = True #行動に探索を取り入れて学習するかどうかの判定
    elif mode == 'eval':
        explor_and_update = False
    obs = env.reset()
    if frames is not None:
        frames.append(Image.fromarray(env.render(mode='rgb_array')))
    reward = 0
    terminal = False
    total_reward = 0
    while not terminal:
        if explor_and_update:
            action = agent.act_and_train(obs, reward)
        else:
            action = agent.act(obs)
        obs, reward, terminal, info = env.step(action)

        total_reward += reward
        if frames is not None:
            frames.append(Image.fromarray(env.render(mode='rgb_array')))
    if explor_and_update:
        agent.stop_episode_and_train(obs, reward)
    env.close()
    return total_reward


if __name__ == '__main__':
    env = gym.make('CartPole-v0') #環境の呼び出し
    env = PositionRewardEnv(env)
    env = PenaltyRewardEnv(env)
    env = ObsForChainerRLEnv(env) #状態をchainer用に変えるラッパー
    #擬似乱数のシードを設定
    env.seed(0)
    space_seed(0)
    np.random.seed(0)

    #Qネットワークの生成
    q_func = chainerrl.q_functions.FCStateQFunctionWithDiscreteAction(
        env.observation_space.shape[0], env.action_space.n,
        n_hidden_layers=2, n_hidden_channels=256,
        nonlinearity=F.relu
    )

    #勾配降下法アルゴリズムの生成
    optimizer = chainer.optimizers.Adam(alpha=1e-5)
    optimizer.setup(q_func)

    gamma = 0.99

    #方策の生成
    explorer = chainerrl.explorers.ConstantEpsilonGreedy(
        epsilon=0.1, random_action_func=env.action_space.sample
    )

    #経験メモリの生成
    replay_buffer = chainerrl.replay_buffer.ReplayBuffer(capacity=10**5)

    #エージェント(DoubleDQN)の生成
    #GPUは使っていません(使用しないほうが高速でした)
    agent = chainerrl.agents.DoubleDQN(
        q_func, optimizer, replay_buffer, gamma, explorer,
        replay_start_size=500, minibatch_size=32,
        update_interval=1, target_update_interval=10000
    )

    total_reward_history = []
    for e in range(1, 10001):
        total_reward = play_episode(env, agent, mode='train')
        total_reward_history.append(total_reward)
        #500エピソードごとに統計情報の表示
        if e % 500 == 0:
            print(f'episode:{e} statistics:{agent.get_statistics()}')

    smoothing = [np.mean([total_reward_history[i: i+100]]) for i in range(10000)]
    plt.figure()
    plt.plot(np.arange(1, 10001), total_reward_history, color='Blue', alpha=0.1)
    plt.plot(np.arange(1, 10001), smoothing, color='Blue')
    plt.show()

    #10回評価モードでエピソードを実行
    for e in range(1, 11):
        frames = []
        play_episode(env, agent, mode='eval', frames=frames)
        frames[0].save(
            'result_' + str(e) + '.gif',
            save_all=True,
            append_images=frames[1:],
            optimize=False,
            duration=1000/50,
            loop=0
        )

結果

10000エピソード学習させ、各エピソードで獲得した報酬の合計をプロットしたグラフがこちらです。
(薄い線が実際の報酬、濃い線が直近100エピソード分の平均を取ったときの報酬を示しています。)

plot.png

また、学習後に探索なしでエピソードを実行させたときの様子がこちらになります。

result.gif

ポールを倒さないようにカートを右に移動させる動きを学習できています。

※最初に少しだけ左に移動させてポールをちょっと右に倒してから、カートを右に移動させることでポールを直立させています。また、ポールを直立させたら徐々に減速させ、カートが右に移動しすぎないよう少しずつ移動しています。思ったより高度な動きになりました。

まとめ

本記事では、OpenAI GymのWrapperクラスを使って環境を改造する方法について書きました。
また、例として、環境の報酬を変えて別の動きを学習させてみました。
最後の学習例はサラっと書いていますが、パラメータ調整にはちょっと苦戦していました。練習としてちょうどいい難易度だったかもしれませんね。

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

Python勉強の記録_190819

やったこと

  • 競技プログラミング続き
  • やさしく始めるラズベリー・パイを読み始めた

学び

  • Global
#local
a = "before"
def sample():
    a = "after"
print(a) #before
sample()
print(a) #before

#global
a = "before"
def sample():
    global a
    a = "after"
print(a) #before
sample()
print(a) #after
  • リストのcountとindex
lst = [1,1,4,6,7]
print(lst.count(1))
#2
print(lst.index(6))
#3
print(lst.index(1))
#0(最初にある要素のインデックスを表示)
print(lst.index(0))
#エラー
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめての Flask #5 ~JSONを返すWebAPIを書こう~

前回: はじめての Flask #4 ~データベースをSQLAlchemyでいじってみよう~

今までの学習を通して、様々なwebアプリが作れるようになったと思います。

さて、今回はいよいよJSONを返すWebAPIを作ろうと思います!

何がうれしいの

WebAPIがあると何がうれしいの?ということですが、大まかに以下の理由があげられます。

・内部処理とビュー(外面)を分けることができる
・さまざまなプラットフォームでの利用に簡単に対応することができる
・細かく処理を分けることができるので、設計自体が非常にわかりやすく、かつ組み立てやすくなる

上記の3つが主に考えられるものです。
それぞれわかりやすく説明してみましょう。

内部処理とビュー(外面)を分けることができる

今までの内容で作ったwebサービスであれば、1つのページに対して1つの動作というペアが完成していました。あるページからほかのページの内容をそのまま取得することはできず、情報の取得はページの遷移でしか行えないものでした。つまり、情報の内容とUI(HTMLなど)は切り離せない状態で返すしかなかったのです。
しかし、JSONという情報のみを扱うデータを返すことができるようになると、その情報の表現はUI次第になります。UIが古臭いならば見てくれのみ改修すればよく、セキュリティに問題があるならばWebAPIのみ変更すればよいのです。
この分離が、WebAPI最大の特徴ともいえます。

さまざまなプラットフォームでの利用に簡単に対応することができる

上記の通り、情報とUIの分離に成功すれば、UIのみそれぞれのに対応するように設計すれば、WebAPI自体はノータッチで再利用できます。
ブラウザ版も、スマホアプリ版も、どのような利用方法であったとしても内部処理は変わらないので、バックエンド開発側もフロントエンド開発側も楽に実装ができます。

細かく処理を分けることができるので、設計自体が非常にわかりやすく、かつ組み立てやすくなる

あたかもプログラム内で関数を定義するかのように、目的の動作を行うエンドポイントを作成すればその組み合わせで容易に処理が実行できます。
目的の異なるエンドポイントを複数作成する方が、互いに複雑に絡み合った長大な単一のエンドポイントを作成・保守するよりも容易なことは想像に難くないはずです。

早速作ろう!

ということで、まず大切なJSONの返し方から学んでいきましょう!

JSONの返し方

from flask import *

app = Flask(__name__)


@app.route("/")
def index():
    return jsonify({"language": "python"})


if __name__ == "__main__":
    app.run(port=8888)

このようにflask.jsonifyにdict型を与えるとそれをJSONにダンプしてくれますので、それを返しましょう。ちなみに、このjsonifyを行ったときはheaderContent-Typeapplication/jsonにしてくれます。ありがとうFlask。

このページに接続すると

{"language": "python"}

というJSONを返します。

ちなみに

from flask import *
import json

app = Flask(__name__)


@app.route("/")
def index():
    return json.dumps({"language": "python"})


if __name__ == "__main__":
    app.run(port=8888)

のように自らjson.dumpsを使ってJSONをダンプすることもできますが、こちらはheaderContent-Typetext/html; charset=utf-8となり、みずからheaderを書き換える必要が出てくるので、flask.jsonifyを使う方が良いです。

ステータスコードも一緒に返そう!

WebAPIを作ると「認証が間違っている!!(400番)」、「お前ちょっとリクエスト送りすぎ!!(429番)」、「ごめん、エラー起きた(500番)」など、200番以外のステータスコードを返したくなることもあります(というか返しましょう)。
そのときには、エンドポイント内のreturnで、ステータスコードの番号も一緒に返しましょう!

from flask import *

app = Flask(__name__)


@app.route("/")
def index():
    return jsonify({"language": "python"}), 418


if __name__ == "__main__":
    app.run(port=8888)

このようにすることで、今回であれば「I'M A TEAPOT」(418番)のステータスコードを返すことができます。

日本語を返したい!

ちなみに、flaskのflask.jsonifyでは、すべての文字はデフォルトではasciiとなります。ですので、日本語を返そうと以下のようなコードを書くと、不都合が起きます。

from flask import *

app = Flask(__name__)


@app.route("/")
def index():
    return jsonify({"language": "パイソン"}), 418


if __name__ == "__main__":
    app.run(port=8888)

帰ってくるJSONは

{"language":"\u30d1\u30a4\u30bd\u30f3"}

となります。わぁ、Unicodeだぁ…。
はい、ですので、文字コードの設定をUTF-8(asciiじゃなくする)にしてあげましょう。

from flask import *

app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False

@app.route("/")
def index():
    return jsonify({"language": "パイソン"}), 418


if __name__ == "__main__":
    app.run(port=8888)

このように、app.config["JSON_AS_ASCII"]Falseにすることで日本語・漢字・そのほか多くの文字をJSONに正しくダンプできます。

これまでの情報と、GET, POST(ほかにもPUTなどのメソッドもありますが、それは他の記事に譲ります)のやり取りを組み合わせることで、WebAPIは作成できます。

それではみなさん、良いPython Lifeを!!

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

Pythonでつくる彼女入門 ~Tinder自動化プロジェクト~ 第4話

目次

やったこと 主な出来事
第1話 自動右スワイプ
第2話 自動メッセージ送信 女性とマッチした
第3話 ライブラリ化 マッチした女性とLINEを交換した
第3.5話 アクセストークンの再取得 これまでのコードではトークンが取得できなくなっていた
第4話 データ収集 LINEの返信が来なくなった

コードはGitHubから閲覧できます。

前回までのあらすじ

  • 自動右スワイプ機能を作成した
  • メッセージ送信を自動化した
  • AWSを用いてコードを定期実行した
  • TinderのAPIを簡単に叩くためのライブラリを作成した

近況

前回LINE交換した子から返事が来なくなった...なんでだ...
気を取り直してコードを書いていきましょう。やはり僕の恋人はPCですね。決して裏切らない。
第3話が課題1を消化したところで終わってしまったので、その続きです。

課題2 イロ・スコアってなに??

第1話のコメント欄にて、@gshirato さんからEloスコアというのものの存在を教えていただきました(@gshiratoさんありがとうございます)。
なんでも、全部の相手を右スワイプしていると女性側に自分のプロフィールが表示されにくくなるとか...。
以下は公式ブログ[1]からの引用です。

Tinderのアルゴリズムのこの部分がLikeとNopeを比較し、相手プロフィールの扱い方に関する類似パターンを見出します。そのデータに基づいて相性がいいと考えられるマッチ候補を表示するために、このアルゴリズムが使われていたのです。
(中略)
現在、Tinderではイロに依存することはありません。もちろんお互いがプロフィールにLikeをつけてマッチが成立したユーザーについてそれぞれのユーザーを検討する場合はイロが重要な役割を果たします。現在のTinderのシステムでは、あなたのプロフィールがLikeやNopeを付けられる都度、あなたに表示されるマッチ候補が調整されています。

どうやらイロ・スコアとうのは自分との相性を数値化したもののようで、現在は使われていないようです。実際、サーバーからの返り値を見ても、それっぽい値は見当たりませんでした。
残念。これがあれば、自分のスコアを高める工夫をするのはもちろん、相手のスコアを確認することで、右スワイプをする前にマッチするかどうかが判断できるかもしれなかったのに。

本当にこのスコア使われてないのかな。最近マッチしなくなってきた気がするんだけど

実際全右スワイプって、(イロ・スコアの存在を抜きにしても)効率悪いんですよね。しっかり数えたわけではないですが、現在まで自動右スワイププログラムを可動させた日数とマッチの数から、右スワイプでマッチする確率は約0.01%。
流石になんとかしたいですよね。

よし、機械学習だ。

データ収集

まずはデータを収集します。どういったプロフィールの人がマッチしやすく、どういった人はマッチしづらいのかをモデルに教えるために、データは必須です。
意外とルールベースな特徴とか見つかったりしないかなぁ。

データベースとストレージ

まずは収集する情報を決めましょう。マッチするかどうかの判断の参考になりうる情報というと...

  • 写真
  • 自己紹介文
  • 学校
  • 仕事
  • 年齢
  • 自分との距離

等でしょうか。やはり写真と自己紹介は外せないですよね。
これらの情報を収集し、保存しておくためのストレージ(あるいはDB)が必要です。
最終的に分析することを考えると、colaboratoryと相性のいいGoogle DriveとGoogle spreadsheetがいいでしょうか。容量も多いし。GASで制御できるし。
全体図は下のようになる予定です。
asset1.png

AWS Lambda

せっかく第3話でライブラリを作ったので、それを活かします。まずは自動右スワイプを行うコードです。

lambda_function.py
import json
import tinpy

#FBemail = "Facebookのメールアドレス"
#FBpass = "Facebookのパスワード"
#token = tinpy.getAccessToken(FBemail, FBpass)
token = "Facebookのトークン"

def lambda_handler(event, context):

    api = tinpy.API(token)

    api.setLocation(35.658034, 139.701636)

    for user in api.getNearbyUsers():
        if api.getLikesRemaining() == 0:
            break
        user.like()

    for match in api.getMatch():
        messages = match.messages
        if len(messages) == 0:
            match.sendMessage(
                "はじめまして{0}さん! マッチありがとうございます!".format(match.name))

これをAWS Lambdaに登録して実行するだけで自動右スワイプが可能です。
続いて、データベースにユーザーの情報を送信するコードと、マッチしたアカウントを送信するコードが必要です。
といっても送信するだけなので別に難しくはありません。

lambda_function.py
import requests

url = "ここにGASのURL(後述)を記載"


def sendProfile(user):
    data = {"id": user.id, "name": user.name,
            "age": user.age, "gender": user.gender}
    data["bio"] = user.bio
    for i in range(len(user.photos)):
        data["photo{0}".format(i)] = user.photos[i]
    data["videos"] = user.videos
    data["schools"] = user.schools
    data["jobs"] = user.jobs
    data["distance_mi"] = user.distance_mi
    with requests.Session() as s:
        print(data)
        s.post(url, data=data)


def sendMatch(match):
    with requests.Session() as s:
        data = {"id": match.id}
        s.get(url, params=data)

urlのパラメーターは、後に作るデータベースのエンドポイントです。
スワイプ情報とマッチ情報の2種類の情報を送る必要があるので、GETとPOSTで区別しています。
データベースを作ってからでないと結局Lambda側のコードは完成しないので、最終的な完成品は後に回すとして、次はデータベースを作成したいと思います。

Google Apps Script

Google Apps Script (GAS)は、GmailやDriveといったGoogleの各種サービスを制御するAPIを兼ね備えたJavaScript実行環境(みたいなもの)です。Googleアカウントを持っていれば無料で使うことができます。
外部とのHTTPS通信や、コードの定期実行ができたり、URLを取得してウェブページを表示したりと、もはやサーバーとして利用が可能(?)なサービスです。

まずは新しいプロジェクトを作成します。GASにはスタンドアロンなプロジェクトとスプレッドシートに紐付いたプロジェクトが存在しますが1、今回はスプレッドシートに紐付いたものを用います。
新しいスプレッドシートを作成して、ツール > スクリプトエディタを選択してください。
図のようなエディタが別タブで立ち上がればOKです。
gas start.png
つづいて、Google Driveの好きな位置に、ファイルを保存するためのフォルダを作っておいてください。

エンドポイント作成

まずは、スプレッドシートのIDと、Google Driveのデータ保存用フォルダのIDを取得します。
ブラウザでGoogle Driveのフォルダを開くと、URLがhttps://drive.google.com/drive/u/0/folders/XXXXXXXXXXXXとなっています。このXXXXXXXXXXXXがフォルダのIDです。
また、先程作成したスプレッドシートのURLを見ると、https://docs.google.com/spreadsheets/d/XXXXXXXXXXXX/edit#gid=0となっていると思います。こちらもXXXXXXXXXXXXがスプレッドシートのIDです。
どちらもメモしておいてください。

つづいて、データ保存用のプログラムを書いていきます。
GASでは、doPost(e), doGet(e)というfunctionを用意することで、外部からPOST, GETでアクセスした際にこれを起動することができます。
ということで、受け取ったデータをSpread SheetのAPIを用いてシートに記入していきます。

tinder.js
sheetId = "スプレッドシートのID";
folderId = "フォルダのID";


//スワイプしたプロフィールの情報を記録する
function doPost(e) {
  var sheet = SpreadsheetApp.openById(sheetId).getSheets()[0];//スプレッドシートにアクセス
  var id = e.parameter.id;
  var name = e.parameter.name;
  var age = e.parameter.age;
  var gender = e.parameter.gender;
  var distance_mi = e.parameter.distance_mi;
  var bio = e.parameter.bio;
  var jobs = e.parameter.jobs;
  var schools = e.parameter.schools;
  var match = 0;
  var timestamp = Math.round((new Date()).getTime() / 1000);
  var datas = [
    [id, name, age, gender, distance_mi, bio, jobs, schools, match, timestamp]
  ]
  var photo0 = e.parameter.photo0;
  var photo1 = e.parameter.photo1;
  var photo2 = e.parameter.photo2;
  var photo3 = e.parameter.photo3;
  var photo4 = e.parameter.photo4;
  var photo5 = e.parameter.photo5;

  var photos = [photo0, photo1, photo2, photo3, photo4, photo5];

  var row = sheet.getLastRow() + 1;//最終行を取得
  sheet.getRange(row, 1, 1, 10).setValues(datas);

  //写真を保存
  for(i=0;i<photos.length;i++){
    var photo = photos[i];
    if (photo != null) {
      var name = id + "-" + i + ".jpg";
      var blob = getImage(photo, name);
      saveFile(blob);
    }
  }
}


function getImage(url, name) {
  var response = UrlFetchApp.fetch(url);//urlにアクセスしてデータを取得
  var response = response.getBlob().setName(name);//
  return response
}

function saveFile(blob) {
  var folder = DriveApp.getFolderById(folderId);
  folder.createFile(blob)
}

//マッチしたプロフィールの情報を記録する
function doGet(e){
  var sheet = SpreadsheetApp.openById(sheetId).getSheets()[0];
  var id = e.parameter.id;

  var ids=sheet.getRange(1,1,row).getValues();//これまでマッチした人すべてのidを取得
  for(i=0;i<ids.length;i++){
    if(id==ids[i][0]){
      sheet.getRange(i+1,9).setValue(1);
      return;
    }
  }
}

コードを作成したら、エディタ上部にある[公開]>[ウェブアプリケーションとして導入]を選択し、

  • プロジェクトバージョン: New
  • 次のユーザーとしてアプリケーションを実行: 自分(hoge @ fuga.com)
  • アプリケーションにアクセスできるユーザー: 全員(匿名ユーザーを含む)

として導入をクリックします。
GAS.png
表示されるURLをメモしておいてください。これがエンドポイントです。

これを先程作成したlambda_fucntion.pyに記入します。
最終的なコードは下記のとおりです。

lambda_function.py
import requests
import json
import tinpy

#FBemail = "Facebookのメールアドレス"
#FBpass = "Facebookのパスワード"
url = "GASのURL"
#token = tinpy.getAccessToken(FBemail, FBpass)
token = "Facebookのトークン"

def lambda_handler(event, context):

    api = tinpy.API(token)

    api.setLocation(37.658034, 139.701636)


    print(api.getLikesRemaining())    

    for user in api.getNearbyUsers():
        if api.getLikesRemaining() == 0:
            break
        user.like()
        sendProfile(user)

    for match in api.getMatch():
        sendMatch(match)
        messages = match.messages
        if len(messages) == 0:
            match.sendMessage(
                "はじめまして{0}さん! マッチありがとうございます!".format(match.name))

    return

def sendProfile(user):
    data = {"id": user.id, "name": user.name,
            "age": user.age, "gender": user.gender}
    data["bio"] = user.bio
    for i in range(len(user.photos)):
        data["photo{0}".format(i)] = user.photos[i]
    data["videos"] = user.videos
    data["schools"] = user.schools
    data["jobs"] = user.jobs
    data["distance_mi"] = user.distance_mi
    with requests.Session() as s:
        print(data)
        s.post(url, data=data)


def sendMatch(match):
    with requests.Session() as s:
        data = {"id": match.id}
        s.get(url, params=data)

あとはこれを保存して定期実行すれば、データ収集はバッチリです!

実行するとこんな感じ↓にdrive.png

次回はいよいよデータ分析です!良いデータが溜まりますように...!

参考文献

[1]https://blog.gotinder.com/powering-tinder-r-the-method-behind-our-matching/
[2]https://tonari-it.com/gas-url-doget-parameter/


  1. 他にもフォームに紐付いたプロジェクトなんかもあります。 

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

Windows2003(with Atom)にPythonを導入する

Windows2003(with Atom)にPythonを導入する

やめましょう.

環境

OS : Windows server 2003 Standerd Endition
CPU : Atom(TM) CPU 330 @ 1.60 GHz
日付 : 2019/8/10

目的

デュアルブートとか仮想化とかを禁止されているけど,Python3環境が欲しい

やること

Python3.4のダウンロード

Pythonの公式サイトから32bit版WindowsのPythonをダウンロード
Python3.4以下じゃないと動かない(らしい).
なお,IE8からではアクセスできなかったのでchromeを入れた.

Pathを通す

コマンドプロンプトで,以下のコマンド打つ.
C:\User\Pythonの部分はさっきダウンロードして解凍したディレクトリを指定.
set path = %path%;C:\User\Python
これで,コマンドプロンプト上でpythonと打てば動くようになっているはず.

pip を使えるようにする

まずはpipを使えるようにする.
これはやらなくても動くかもしれない.
python -m ensurepip

setuptoolsは必ずアップグレードしときましょう.
python -m pip install --upgrade pip setuptools

pipが使える状態になれば,以下のようにpipでパッケージインストールができる.
python -m pip install numpy==1.15.0
python -m pip install pandas==0.16.0

ダメだったこと

Anacondaが入れば早いのだが以下のエラーで動かなかった.
プロシージャエントリポイントGetFinalPathNameByHandleWがダイナミックリンクライブラリKERNEL32.dllから見つかりませんでした.
そもそもこのWindowsにはpython3.4以前しか動かない(らしい)のでそっちが原因かもしれない.

ちなみにLightGBMは32bitに対応していない.

最後に

買い換えた方が良いと思います.

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

AtCoder Grand Contest 037 参戦記

AtCoder Grand Contest 037 参戦記

A - Dividing a String

52分で突破. そもそも山でバスに間に合わず、蒲田駅についたのが21時5分って感じで、15分以上遅れてスタート. 問題文をちゃんと見ていなく、最初はすべての分割文字がユニークだと勘違いして、なんて難しい問題だって唸ってた. 正しく題意を理解してサラッと書いたのが以下. ダメでも良いやで流して AC. 正直なところ、最後と最後の一個手前が違うことを確認してないので、意地悪なテストケースを用意されると転けそうな気がする. 後から、分割文字は1文字→1文字、1文字→2文字、2文字→1文字の3パターンしか無いのかーって思った.

s = input()
t = ''
k = 0
prev = ''
for i in range(len(s)):
  t += s[i]
  if prev != t:
    k += 1
    prev = t
    t = ''
print(k)

C - Numbers on a Circle

敗退. B はもう問題文を見ただけで無理って思ったので飛ばしてこっちをやってた. 問題を見た感想は、「えー、どの順でやればいいの. 総当りしたら組合せ爆発しない?」って気分だった. とりあえず順方向だと最小どころかそもそもAからBにたどり着ける気がしないので、逆向きにやっていくことにし、回数最小ってことはできる限り小さくしない(小さいと引ける数が減って最小にならない)のがいいのかなと、一番大きいのを順に置き換えていくことを計画. 一番大きいのをどんどん取り出すために順序付きキューを使って実装してみた. TLE にはなるものの WA はでず、方針はこれ出会ってるかと思ったものの、処理量を減らす方法が全く思いつかずタイムアップ.

def main():
  import sys
  from heapq import heappush, heappop, heapify
  _int = int
  n = _int(input())
  a = [_int(e) for e in input().split()]
  b = [_int(e) for e in input().split()]
  result = 0
  finished = 0
  hq = [(-b[i], i) for i in range(n)]
  heapify(hq)
  while True:
    _, i = heappop(hq)
    if a[i] == b[i]:
      finished += 1
      continue
    if i == 0:
      b[i] -= b[n - 1] + b[1]
    elif i == n - 1:
      b[i] -= b[n - 2] + b[0]
    else:
      b[i] -= b[i - 1] + b[i + 1]
    if a[i] > b[i]:
      print(-1)
      sys.exit()
    result += 1
    if a[i] == b[i]:
      finished += 1
      if finished == n:
        print(result)
        sys.exit()
    else:
      heappush(hq, (-b[i], i))
main()

解説 PDF を読んで、一気に減らして大丈夫なのか、本当にこれで最小になるのかと思いつつ、実装したら AC. 未だに本当に最小回数なんだろうかって思っている自分がいる.

def main():
  import sys
  _int = int
  n = _int(input())
  a = [_int(e) for e in input().split()]
  b = [_int(e) for e in input().split()]
  result = 0
  q = [i for i in range(n) if b[i] != a[i]]
  while len(q) != 0:
    nq = []
    c = 0
    for i in q:
      if i == 0 or i == n - 1:
        j = b[(n + i - 1) % n] + b[(n + i + 1) % n]
      else:
        j = b[i - 1] + b[i + 1]
      if j > b[i] - a[i]:
        nq.append(i)
        continue
      c += 1
      k = (b[i] - a[i]) // j
      result += k
      b[i] -= j * k
      if a[i] != b[i]:
        nq.append(i)
    if c == 0 and len(nq) != 0:
      print(-1)
      sys.exit()
    q = nq
  print(result)
main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】【Django】CSRFトークンエラーの検知テスト

概要

Djangoでテンプレートのフォームを手作業で作ると時々やらかすミス、「CSRFトークン用タグのつけ忘れ」。
runserverでテスト環境を動かすことで検知することが多いですが、ユニットテストではどこで検知できるかを確かめてみました。

目次

  1. ソースコード
    1. sampleアプリケーション
    2. config(プロジェクト設定)
    3. テスト
  2. テストの実行結果
  3. 得られた知見

ソースコード

DjangoテストフレームワークでCSRFトークンエラーを検出する

  • config
    • プロジェクト設定
  • sample
    • フォームで入力したメッセージを保存するだけのサンプルアプリケーション
  • templates
    • HTMLテンプレート
  • tests
    • ユニットテスト
    • sample:django.test.testcases.TestCaseを用いたテスト
    • e2e:django.test.testcases.LiveServerTestCaseとSelenium WebDriverを用いたテスト

sampleアプリケーション

モデル sample/models.py

単純にメッセージだけを保持するモデルです。

from django.db import models

# Create your models here.
class Sample(models.Model):
    message = models.CharField(verbose_name='メッセージ', max_length=255)

    """サンプルアプリケーションモデル"""
    class Meta:
        # テーブル名
        db_table = 'sample'

フォーム sample/fomrms.py

sample/models.pyで定義したSampleモデルとdjango.models.ModelFormを用いて、メッセージ用のフォームを定義します。

from django import forms
from .models import Sample

class SampleForm(forms.ModelForm):
    """サンプルフォーム"""
    class Meta:
        model = Sample
        fields = ('message',)
        widgets = {
            'message': forms.Textarea(attrs={'placeholder': 'メッセージ'})
        }

ビュー sample/views.py

sample/forms.pyで定義したSampleFormを用いたビュークラスを定義します。

from django.shortcuts import render,redirect
from .forms import SampleForm
from django.urls import reverse
from django.views import View

class SampleFormView(View):

    # Create your views here.
    def get(self, request, *args, **kwargs):
        context = {
            'form': SampleForm()
        }
        return render(request, 'sample/index.html', context)

    def post(self, request, *args, **kwargs):
        form = SampleForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect(reverse('sample:index'))

        context = {
            'form': form
        }
        return render(request, 'sample/index.html', context)

sampleFromView = SampleFormView.as_view()

HTMLテンプレート templates/sample/index.py

単純に入力フォームと「送信」サブミットボタンを表示するだけです。
今回はCSRFトークンのエラーをテストするため、意図的に{{ csrf_token }}を外しています。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>Form Sample</title>
    </head>
    <body>
        <form method="POST" action="{% url 'sample:index' %}">
            {% for field in form %}
            <label>{{ field.label_tag }}</label>
            {{ field }}
            {% endfor %}
            <input type="submit" value="送信" />
        </form>
    </body>
</html>

アプリ内のURL設定 sample/urls.py

作成したビュークラスにアクセスするURLを定義します。

from django.urls import path
from . import views

app_name='sample'
urlpatterns = [
    path('', views.sampleFromView, name="index")
]

config(プロジェクト設定)

プロジェクト設定 config/settings.py

以下の設定を追加します。

  • 作成したsampleアプリケーションをインストールする
  • HTMLテンプレートをプロジェクト直下のtemplatesディレクトリから読み込む
  • 言語設定を「日本語」に設定する
  • タイムゾーン設定を「東京」に設定する
(省略)
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sample',
]

……

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates')
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

……

# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'ja-JP'

TIME_ZONE = 'Asia/Tokyo'

(省略)

プロジェクトのURL設定 config/urls.py

sampleプロジェクトで作成したURL設定を読み込み、トップページでアクセスできるように設定しています。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sample.urls')),
]

テスト

sample

django.test.testcases.Testcaseを用いたテストを実施します。
django.test.testcases.Testcaseに用意されたdjango.test.Clientクラスのオブジェクトself.clientでは、CSRFトークンのチェックが行われないため、clientをCSRFトークンのチェックを行うように再設定し、サンプルアプリケーションにGET、POSTを行い、GET時に指定したテンプレートの表示、POST時にHTTPステータスコードが403になることを確認します。

参考ページ:
リクエストの作成:テストツール | Django ドキュメント | Django
SimpleTestCase:django.test.testcases | Django ドキュメント | Django

tests/sample/test_view.py

from django.test.testcases import TestCase
from django.urls import reverse
from django.test.client import Client

class SampleViewTest(TestCase):

    def _pre_setup(self):
        super()._pre_setup()
        self.client = Client(enforce_csrf_checks=True)

    def test_get_index_01(self):
        response = self.client.get(reverse('sample:index'))
        self.assertTemplateUsed(response, 'sample/index.html')

    def test_post_index_01(self):
        response = self.client.post(reverse('sample:index'), data={})
        # If csrf_token was template given.
        # self.assertTemplateUsed(response, 'sample/index.html')
        # If csrf_token was't template given.
        self.assertEquals(403, response.status_code)

    def test_post_index_02(self):
        response = self.client.post(reverse('sample:index'), data={'message': 'Test Message'})
        # If csrf_token was template given.
        # self.assertRedirects(response, reverse('sample:index'))
        # If csrf_token was't template given.
        self.assertEquals(403, response.status_code)

e2e

django.test.testcases.LiveServerTestCaseとSelenium WebDriverを用いたテストを実施します。

tests/e2e/test_index.py

from django.test.testcases import LiveServerTestCase
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LiveServerIndexTest(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        options = Options()
        options.add_argument('--headless')
        cls.selenium = webdriver.Chrome(options=options)
        cls.selenium.implicitly_wait(10)

    def test_index_01(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/'))
        self.assertTemplateUsed('sample/index.html')
        self.assertEquals('Form Sample', self.selenium.title)

    def test_index_02(self):
        self.selenium.get('%s%s' % (self.live_server_url, '/'))
        message_elem = self.selenium.find_element_by_css_selector('form textarea[name="message"]')
        message_elem.send_keys("Test Message")
        submit_elem = self.selenium.find_element_by_css_selector('form input[type="submit"]')
        submit_elem.click()
        WebDriverWait(self.selenium, 15).until(EC.visibility_of_all_elements_located)

        # assert Submit Success
        # self.assertEquals('Form Sample', self.selenium.title)

        # assert Submit 403 Error(CSRF Token Error)
        self.assertTrue('403' in self.selenium.title)

    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()

テストの実行結果

tests/sample/test_views.py
スクリーンショット 2019-08-20 18.08.56.png

test/e2e/test_index.py
スクリーンショット 2019-08-20 18.09.33.png

得られた知見

  • django.test.testcases.TestCaseを用いたユニットテストで、CSRFトークンエラーを検知することは可能
    • ただし、_pre_setup()メソッドをオーバーライドし、self.clientをCSRFトークンチェック有効(enforce_csrf_checks=True)のクライアントに上書きする必要がある
      • self.client自体はdjango.test.testcases.TestCaseクラスの親クラスであるdjango.test.testcases.SimpleTestCaseで定義されている
      • django.test.Clientenforce_csrf_checks引数はデフォルトでFalseであるため、django.test.testcases.SimpleTestCase_pre_setup()メソッドで生成されるself.clientオブジェクトは常にCSRFトークンチェック無効になっている
      • このため、テストクラス内で_pre_setup()メソッドをオーバーライドし、self.clientenforce_csrf_checks=Trueであるクライアントオブジェクトで上書きする必要が生じる
  • LiveServerTestCaseとSelenium WebDriverを用いたユニットテストで、CSRFトークンエラーを検知することは可能
    • Selenium WebDriverではHTTPステータスコードを取得することはできないため、画面のタイトル等を用いて403エラーを検知する必要がある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python class template 解説(オブジェクト指向を解説)

メリットから覚えるclass

pythonを使ってAddress Book classを作ってみた:fountain:

対象となるオブジェクトは、一般的な住所録です。例

ID 名前 性別 血液型 生年月日 携帯番号 メール 郵便番号 住所
1 関 波子 AB 1980/7/31 090-7787-3784 sk@eaccess.net 135-0034 東京都江東区永代8-1-4
2 小倉 準司 A 1973/10/8 junzi-kokura@eaccess.net 071-1544 北海道上川郡東神楽町14号6-13-1
3 西村 有紀子 O 1972/12/5 090-5165-2074 okikuy1972@livedoor.com 635-0805 奈良県北葛城郡広陵町萱野5-2-7 ヴェルテックス萱野 1009
4 谷 僧三郎 B 1989/1/17 090-3781-1181 suzbrutn@dsn.ad.jp 520-0011 滋賀県大津市南志賀2868
5 山口 和久 A 1973/2/21 070-4790-1232 kazuhisa73@geocities.com 012-0813 秋田県湯沢市前森1-11-7
6 米田 一生 O 1984/7/25 issei07@dsn.ad.jp 264-0029 千葉県千葉市若葉区桜木北4-13-4
7 村松 希美江 O 1980/11/21 090-4849-6939 kimie.muramatu@odn.ne.jp 616-8151 京都府京都市右京区太秦帷子ヶ辻町6-6-7
8 小野寺 眞八 O 1986/4/28 090-1457-0772 ondr.sny@gmo-media.jp 527-0135 滋賀県東近江市横溝町6-6-7
9 志村 陽一郎 O 1975/7/8 smr-yutru@dion.ne.jp 048-1321 北海道磯谷郡蘭越町湯里5-9-5 ル・メール湯里
10 鎌田 敏美 B 1993/6/9 090-5690-8749 kamata0609@example.com 409-0115 山梨県上野原市松留4631

クラスを定義してみましょう。

address.py
class AddressBook:
    def __init__(self,str): #タブ区切りデータ
        x=str.split('\t')
        self.ID             =x[0]
        self.名前     =x[1]
        self.性別     =x[2]
        self.血液型      =x[3]
        self.生年月日       =x[4]
        self.携帯番号       =x[5]
        self.メール      =x[6]
        self.郵便番号       =x[7]
        self.住所     =x[8]
  #文字列データからのデータのセット
    def sets(self,ID,名前,性別,血液型,生年月日,携帯番号,メール,郵便番号,住所):
        self.ID            =    ID
        self.名前          =  名前
        self.性別          =  性別
        self.血液型        = 血液型
        self.生年月日      =    生年月日
        self.携帯番号      =    携帯番号
        self.メール        = メール
        self.郵便番号      =    郵便番号
        self.住所          =  住所
    def getDict(self):  #dict形式を返す
        return self.__dict__
    def gets(self): # 内部データの配列化
        return [self.ID,self.名前,self.性別,self.血液型,self.生年月日,self.携帯番号,self.メール,self.郵便番号,self.住所]
    def out(self):
        print(",".join(self.gets()))
test.py
d='''1  関 波子  女 AB  1980/7/31   090-7787-3784   sk@eaccess.net  135-0034    東京都江東区永代8-1-4
2   小倉 準司   男 A   1973/10/8       junzi-kokura@eaccess.net    071-1544    北海道上川郡東神楽町14号6-13-1
3   西村 有紀子    女 O   1972/12/5   090-5165-2074   okikuy1972@livedoor.com 635-0805    奈良県北葛城郡広陵町萱野5-2-7 ヴェルテックス萱野 1009
4   谷 僧三郎   男 B   1989/1/17   090-3781-1181   suzbrutn@dsn.ad.jp  520-0011    滋賀県大津市南志賀2868
5   山口 和久   男 A   1973/2/21   070-4790-1232   kazuhisa73@geocities.com    012-0813    秋田県湯沢市前森1-11-7
6   米田 一生   男 O   1984/7/25       issei07@dsn.ad.jp   264-0029    千葉県千葉市若葉区桜木北4-13-4
7   村松 希美江    女 O   1980/11/21  090-4849-6939   kimie.muramatu@odn.ne.jp    616-8151    京都府京都市右京区太秦帷子ヶ辻町6-6-7
8   小野寺 眞八    男 O   1986/4/28   090-1457-0772   ondr.sny@gmo-media.jp   527-0135    滋賀県東近江市横溝町6-6-7
9   志村 陽一郎    男 O   1975/7/8        smr-yutru@dion.ne.jp    048-1321    北海道磯谷郡蘭越町湯里5-9-5 ル・メール湯里
10  鎌田 敏美   女 B   1993/6/9    090-5690-8749   kamata0609@example.com  409-0115    山梨県上野原市松留4631'''
abook=[AddressBook(x) for x in  d.split('\n')]
for x in abook:
   x.out()
result.txt
1,関 波子,女,AB,1980/7/31,090-7787-3784,sk@eaccess.net,135-0034,東京都江東区永代8-1-4
2,小倉 準司,男,A,1973/10/8,,junzi-kokura@eaccess.net,071-1544,北海道上川郡東神楽町14号6-13-1
3,西村 有紀子,女,O,1972/12/5,090-5165-2074,okikuy1972@livedoor.com,635-0805,奈良県北葛城郡広陵町萱野5-2-7 ヴェルテックス萱野 1009
4,谷 僧三郎,男,B,1989/1/17,090-3781-1181,suzbrutn@dsn.ad.jp,520-0011,滋賀県大津市南志賀2868
5,山口 和久,男,A,1973/2/21,070-4790-1232,kazuhisa73@geocities.com,012-0813,秋田県湯沢市前森1-11-7
6,米田 一生,男,O,1984/7/25,,issei07@dsn.ad.jp,264-0029,千葉県千葉市若葉区桜木北4-13-4
7,村松 希美江,女,O,1980/11/21,090-4849-6939,kimie.muramatu@odn.ne.jp,616-8151,京都府京都市右京区太秦帷子ヶ辻町6-6-7
8,小野寺 眞八,男,O,1986/4/28,090-1457-0772,ondr.sny@gmo-media.jp,527-0135,滋賀県東近江市横溝町6-6-7
9,志村 陽一郎,男,O,1975/7/8,,smr-yutru@dion.ne.jp,048-1321,北海道磯谷郡蘭越町湯里5-9-5 ル・メール湯里
10,鎌田 敏美,女,B,1993/6/9,090-5690-8749,kamata0609@example.com,409-0115,山梨県上野原市松留4631
Press any key to continue . . .

女性のみを表示するには、

for x in abook:
   if x.性別=='女': x.out()

1,関 波子,女,AB,1980/7/31,090-7787-3784,sk@eaccess.net,135-0034,東京都江東区永代8-1-4
3,西村 有紀子,女,O,1972/12/5,090-5165-2074,okikuy1972@livedoor.com,635-0805,奈良県北葛城郡広陵町萱野5-2-7 ヴェルテックス萱野 1009
7,村松 希美江,女,O,1980/11/21,090-4849-6939,kimie.muramatu@odn.ne.jp,616-8151,京都府京都市右京区太秦帷子ヶ辻町6-6-7
10,鎌田 敏美,女,B,1993/6/9,090-5690-8749,kamata0609@example.com,409-0115,山梨県上野原市松留4631
Press any key to continue . . .

このようにすっきり書くことができる。

結局やりたかったことは、クラスを定義するとJson形式にすぐに変換できるORM(Object-relational mapping)が簡単になることなんだ。

for x in abook:
    print(x.getDict())

{'ID': '1', '名前': '関 波子', '性別': '女', '血液型': 'AB', '生年月日': '1980/7/31', '携帯番号': '090-7787-3784', 'メール': 'sk@eaccess.net', '郵便番号': '135-0034', '住所': '東京都江東区永代8-1-4'}
{'ID': '2', '名前': '小倉 準司', '性別': '男', '血液型': 'A', '生年月日': '1973/10/8', '携帯番号': '', 'メール': 'junzi-kokura@eaccess.net', '郵便番号': '071-1544', '住所': '北海道上川郡東神楽町14号6-13-1'}
{'ID': '3', '名前': '西村 有紀子', '性別': '女', '血液型': 'O', '生年月日': '1972/12/5', '携帯番号': '090-5165-2074', ' メール': 'okikuy1972@livedoor.com', '郵便番号': '635-0805', '住所': '奈良県北葛城郡広陵町萱野5-2-7 ヴェルテックス萱野 1009'}
{'ID': '4', '名前': '谷 僧三郎', '性別': '男', '血液型': 'B', '生年月日': '1989/1/17', '携帯番号': '090-3781-1181', 'メ ール': 'suzbrutn@dsn.ad.jp', '郵便番号': '520-0011', '住所': '滋賀県大津市南志賀2868'}
{'ID': '5', '名前': '山口 和久', '性別': '男', '血液型': 'A', '生年月日': '1973/2/21', '携帯番号': '070-4790-1232', 'メ ール': 'kazuhisa73@geocities.com', '郵便番号': '012-0813', '住所': '秋田県湯沢市前森1-11-7'}
{'ID': '6', '名前': '米田 一生', '性別': '男', '血液型': 'O', '生年月日': '1984/7/25', '携帯番号': '', 'メール': 'issei07@dsn.ad.jp', '郵便番号': '264-0029', '住所': '千葉県千葉市若葉区桜木北4-13-4'}
{'ID': '7', '名前': '村松 希美江', '性別': '女', '血液型': 'O', '生年月日': '1980/11/21', '携帯番号': '090-4849-6939', 'メール': 'kimie.muramatu@odn.ne.jp', '郵便番号': '616-8151', '住所': '京都府京都市右京区太秦帷子ヶ辻町6-6-7'}
{'ID': '8', '名前': '小野寺 眞八', '性別': '男', '血液型': 'O', '生年月日': '1986/4/28', '携帯番号': '090-1457-0772', ' メール': 'ondr.sny@gmo-media.jp', '郵便番号': '527-0135', '住所': '滋賀県東近江市横溝町6-6-7'}
{'ID': '9', '名前': '志村 陽一郎', '性別': '男', '血液型': 'O', '生年月日': '1975/7/8', '携帯番号': '', 'メール': 'smr-yutru@dion.ne.jp', '郵便番号': '048-1321', '住所': '北海道磯谷郡蘭越町湯里5-9-5 ル・メール湯里'}
{'ID': '10', '名前': '鎌田 敏美', '性別': '女', '血液型': 'B', '生年月日': '1993/6/9', '携帯番号': '090-5690-8749', 'メ ール': 'kamata0609@example.com', '郵便番号': '409-0115', '住所': '山梨県上野原市松留4631'}
Press any key to continue . . .

このような例題書いてくれないとオブジェクト指向のメリット感じないよね!

「いいよね」よろしく

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

対数計算

python
>>> import math

底が2の2の対数 $\log_2 {2}$

python
>>> math.log(2,2) 
1.0

底が2の3の対数 $\log_2 {3}$

python
>>> math.log(3,2)
1.5849625007211563

底が2の4の対数 $\log_2 {4}$

python
>>> math.log(4,2)
2.0

底が2の8の対数 $\log_2 {8}$

python
>>> math.log(8,2)
3.0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Colaboratory上でアニメーションを表示する

TL;DR

Google Colaboratory上で何かイメージを描画して、それをアニメーションにするなら、ファイルを連番で吐いてapngにしてしまうのが楽ちん。

はじめに

Google Colab上で何か計算して、その計算結果をアニメーションとして見せたい。普通にやるならmatplotlibで可視化するのだろうが、これをColab上でアニメーションにするのはわりと面倒くさい。そもそもColab上で画像を表示するのも、一度ファイルに吐いてから表示するのが楽だったりする。

で、アニメーションについてもいろいろ試行錯誤したのだが、結局のところ一度連番ファイルで吐いてしまってから、アニメPNG(APNG)にしてしまうのが一番楽だった。

サンプルコード

APNGのインストール

Google Colabの最初のセルでAPNGをインストールする。

!pip install APNG

おそらく問題なくインストールされて「Successfully installed APNG-0.3.3」とか表示されるはず。

連番ファイルの出力

なんでもよいので、連番のPNGファイルを吐くコードを書こう。以下は円周上を円が周回するだけのアニメーション。

from apng import APNG
from math import cos, pi, sin
from PIL import Image, ImageDraw
import IPython

def save(index, frames):
    filename = "file%02d.png" % index
    im = Image.new("RGB", (100, 100), (255, 255, 255))
    draw = ImageDraw.Draw(im)
    x = 30*cos(2*pi*index/frames) + 50
    y = 30*sin(2*pi*index/frames) + 50
    draw.ellipse((20,20,80,80),outline=(0,0,0))
    draw.ellipse((x-5, y-5, x+5, y+5), fill=(0, 0, 255))
    im.save(filename)
    return filename

後でAPNGにわたすので、ファイル名を返している。

APNGに渡してアニメーションを作成

APNGは、ファイルリストを食わすとAPNGにしてくれるメソッドfrom_filesがあるので、それにファイルリストを食わせて、saveで適当なファイル名で保存する。

保存したファイルはIPython.display.Imageでそのまま表示できる。

files = []
frames = 50
for i in range(frames):
    files.append(save(i, frames))
APNG.from_files(files, delay=100).save("animation.png")
IPython.display.Image("animation.png")

実行結果はこんな感じ。

animation.gif

セルを実行したら、無事にGoogle Colab上でアニメーションが表示された。

まとめ

Google Colab上でアニメーションを表示させる方法を紹介した。数値計算のシミュレーションや、何か動くものの可視化なんかをする場合、Matplotlibで頑張るより、PILで連番png吐いてAPNG作っちゃう方が楽だと思う。

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

Pythonで始める機械学習 教師あり学習のまとめ

教師あり学習 各モデルの特徴

最近傍方

  • 小さいデータに関しては良いベースラインとなる
  • 説明が容易

線形モデル

  • 最初に試すべきアルゴリズム
  • 大きなデータ,高次元なデータに適している

ナイーブベイズ

  • クラス分類にしか使えない
  • 線形モデルよりもさらに高速だが精度が劣ることが多い
  • 大きなデータ,高次元なデータに適している

決定木

  • 非常に高速
  • データのスケールを考慮する必要がない
  • 可視化が可能で説明が容易

ランダムフォレスト

  • 決定木より高速で頑健で強力
  • データのスケールを考慮する必要がない
  • 高次元の疎なデータには適さない

勾配ブースティング決定木

  • ランダムフォレストより少し精度が高い
  • ランダムフォレストより訓練に時間がかかるが予測は早く,メモリ使用量も小さい
  • ランダムフォレストよりパラメータに敏感

サポートベクタマシン

  • 同じような意味を持つ特徴量からなる中規模のデータセットに対して強力
  • データのスケール調整が必要
  • パラメータに敏感

ニューラルネットワーク

  • 非常に複雑なモデルを構築できる
  • 大きなデータセットに有効
  • データのスケールを調整する必要がある
  • パラメータに敏感
  • 大きなモデルでは訓練に時間がかかる

教師あり学習 各モデルの使い方

  • 新しいデータセットを扱う際には,まず線形モデルやナイーブベイズや最近傍方などの簡単なモデルでどの程度の精度かを確認する
  • データを深く理解したのちに,ランダムフォレストや勾配ブースティング,SVM,ニューラルネットワークなどの複雑なモデルを使う
  • モデルの使いわけに加えパラメータ調整や解析を行う
  • これを確認するため,回帰にはboston_housingやdiabetesデータセットが,多クラス分類にはdigitsデータセットなど,様々なデータセットに様々なアルゴリズムを適用してみるのが良い
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで始める機械学習 クラス分類器の不確実性推定

クラス分類器の不確実性推定

  • scikit-learnの有用な機能としてクラス分類器の予測に対する不確実性推定機能(あるテストポイントに対してクラス分類器が出力する予測クラスだけでなく,その予測がどの程度正確なのかを知る)がある
  • クラス分類器の不確実性推定に用いられる関数としてdecision_functionpredict_probaの2つがある
  • これら2つの関数の動きを次の例で見ていく

決定関数(Decision Function)

In[103]:
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.datasets import make_circles
X, y = make_circles(noise=.25, factor=.5, random_state=1)

y_named = np.array(["blue", "red"])[y]

# 文字列の中で \ を使うことで改行できる
X_train, X_test, y_train_named, y_test_named, y_train, y_test = \
    train_test_split(X, y_named, y, random_state=0)

gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train_named)


In[104]:
print("X_test.shape: {}".format(X_test.shape))
print("Decision function shape: {}".format(gbrt.decision_function(X_test).shape))

Out[104]:
X_test.shape: (25, 2)
Decision function shape: (25,)
  • この値にはデータポイントが陽性(クラス1)であるとモデルが信じている度合いがエンコードされており,正であれば陽性,負であれば陰性を意味する
In[105]:
print("Decision fuction:\n{}".format(gbrt.decision_function(X_test)[:6]))

Out[105]: # 符号だけを見れば予測クラスがわかる redが正,blueが負
Decision fuction:
[ 4.13592629 -1.7016989  -3.95106099 -3.62599351  4.28986668  3.66166106]


In[106]:
print("Thresholded decision function:\n{}".format(gbrt.decision_function(X_test) > 0))
print("Prediction:\n{}".format(gbrt.predict(X_test)))

Out[106]:
Thresholded decision function:  # 決定関数の値に閾値を適用して真偽に分類したもの
[ True False False False  True  True False  True  True  True False  True
  True False  True False False False  True  True  True  True  True False
 False]
Prediction:  # 予測結果
['red' 'blue' 'blue' 'blue' 'red' 'red' 'blue' 'red' 'red' 'red' 'blue'
 'red' 'red' 'blue' 'red' 'blue' 'blue' 'blue' 'red' 'red' 'red' 'red'
 'red' 'blue' 'blue']
  • 2クラス分類では陰性クラスがclasses_属性の第1エントリに,陽性クラスがclasses_属性の第2エントリになる
  • predictと同じ結果を再現する場合はclasses_属性を使う
In[107]:
# 「Thresholded decision function」のTrue/Falseを0/1に変換する
greater_zero = (gbrt.decision_function(X_test) > 0).astype(int)
# 0/1をclasses_のインデックスに使う
pred = gbrt.classes_[greater_zero]
# predはgbrt.predictの出力と同じになる
print("pred is equal to predictions: {}".format(np.all(pred == gbrt.predict(X_test))))

Out[107]:  # predとpredictionが一致
pred is equal to predictions: True


In[108]:
decision_function = gbrt.decision_function(X_test)
print("Decision function minimum: {:.2f} maximum: {:.2f}".format(np.min(decision_function), np.max(decision_function)))

Out[108]:  # 決定関数の最小値と最大値
Decision function minimum: -7.69 maximum: 4.29
  • このようにdecision_functionの結果はどのようなスケールで表示されるかわからないため解釈が難しい
  • この結果を次に図で示す
In[109]:
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
mglearn.tools.plot_2d_separator(gbrt, X, ax=axes[0], alpha=.4, fill=True, cm=mglearn.cm2)
scores_image = mglearn.tools.plot_2d_scores(gbrt, X, ax=axes[1], alpha=.4, cm=mglearn.ReBl)

for ax in axes:
    # 訓練データポイントとテストデータポイントをプロット
    mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^', ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o', ax=ax)
    ax.set_xlabel("Feature 0")
    ax.set_ylabel("Feature 1")
cbar = plt.colorbar(scores_image, ax=axes.tolist())
axes[0].legend(["Test class 0", "Test class 1", "Train class 0", "Train class 1"], ncol=4, loc=(.1, 1.1))

Out[109]:
ダウンロード (42).png

  • 訓練データポイントは円で,テストデータポイントは三角で表している
  • 勾配ブースティングモデルの決定境界(左)と確信度(右)
  • ここでは確信度も表しているが,このように可視化してもわかりにくい
  • もう一つ,クラス分類器の不確実性推定に用いられる関数としてpredict_probaがあり,こちらの方がdecision_functionよりも理解しやすい
  • predict_probaの出力配列の形は2クラス分類では常に(n_samples, 2)になる
  • 次に例を示す
In[110]:
print("Shape of probabilities: {}".format(gbrt.predict_proba(X_test).shape))

Out[110]:
Shape of probabilities: (25, 2)

In[111]:
# predict_probaの出力の最初の数行を見る
print("Predicted probabilities:\n{}".format(gbrt.predict_proba(X_test[:6])))

Out[111]:  # 予測確率
Predicted probabilities:
[[0.01573626 0.98426374]
 [0.84575649 0.15424351]
 [0.98112869 0.01887131]
 [0.97406775 0.02593225]
 [0.01352142 0.98647858]
 [0.02504637 0.97495363]]
  • 第1エントリは第1クラスの予測確率,第2エントリは第2クラスの予測確率である
  • 第1エントリと第2エントリ数値は確率であるため,双方の和は常に1となる
  • 50%以上の確信度をもつクラスが予測クラスとなる
  • これを可視化したもの(データセットの境界線とクラス1になる確率)を示す
In[112]:
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

mglearn.tools.plot_2d_separator(gbrt, X, ax=axes[0], alpha=.4, fill=True, cm=mglearn.cm2)
scores_image = mglearn.tools.plot_2d_scores(gbrt, X, ax=axes[1], alpha=.5, cm=mglearn.ReBl, function='predict_proba')

for ax in axes:
    mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^', ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o', ax=ax)
    ax.set_xlabel("Feature 0")
    ax.set_ylabel("Feature 1")
cbar = plt.colorbar(scores_image, ax=axes.tolist())
axes[0].legend(["Test class 0", "Test class 1", "Train class 0", "Train class 1"], ncol=4, loc=(.1, 1.1))

Out[112]:
ダウンロード (43).png

  • 決定境界(左)と確信度(右)を表しており,decision_functionよりも境界がはっきりしていてわかりやすい
  • そのため,わずかに存在する確信度が低い領域もはっきりとわかる
  • このようにして2クラス分類の不確実性推定の結果を確認できる
  • 次に多クラス分類の不確実性を見てみる
In[114]:
from sklearn.datasets import load_iris

iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=42)

gbrt = GradientBoostingClassifier(learning_rate=0.01, random_state=0)
gbrt.fit(X_train, y_train)


print("Decision function shape: {}".format(gbrt.decision_function(X_test).shape))
print("Decision function:\n{}".format(gbrt.decision_function(X_test)[:6, :]))

Out[114]:
Decision function shape: (38, 3)  # (n_samples, n_classes)の形,つまりサンプル数とクラス数である
Decision function:
[[-0.52931069  1.46560359 -0.50448467]
 [ 1.51154215 -0.49561142 -0.50310736]
 [-0.52379401 -0.4676268   1.51953786]
 [-0.52931069  1.46560359 -0.50448467]
 [-0.53107259  1.28190451  0.21510024]
 [ 1.51154215 -0.49561142 -0.50310736]]
  • 2クラス分類の場合と同様に,多クラス分類の場合でも値は各クラスに対する確信度を表している(確信度が高いクラスが予測クラスとされる)
In[115]:
print("Argmax of decision function:\n{}".format(np.argmax(gbrt.decision_function(X_test), axis=1)))
print("Predictions:\n{}".format(gbrt.predict(X_test)))

Out[115]:
Argmax of decision function:  # 決定関数のargmax
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1
 0]
Predictions:  # 予測値
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1
 0]

In[116]:
# predict_probaの結果に最初の数行を表示
print("Predicted probabilities:\n{}".format(gbrt.predict_proba(X_test)[:6]))
# sumでリスト内の値の合計が1になることを確認する
print("Sums: {}".format(gbrt.predict_proba(X_test)[:6].sum(axis=1)))

Out[116]:
Predicted probabilities:
[[0.10664722 0.7840248  0.10932798]
 [0.78880668 0.10599243 0.10520089]
 [0.10231173 0.10822274 0.78946553]
 [0.10664722 0.7840248  0.10932798]
 [0.10825347 0.66344934 0.22829719]
 [0.78880668 0.10599243 0.10520089]]
Sums: [1. 1. 1. 1. 1. 1.]

In[117]:  # predict_probaのargmaxを取ることで予測クラスを再現する
print("Argmax of predicted probabilities:\n{}".format(np.argmax(gbrt.predict_proba(X_test), axis=1)))
print("Predictions:\n{}".format(gbrt.predict(X_test)))

Out[117]:
Argmax of predicted probabilities:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1
 0]
Predictions:
[1 0 2 1 1 0 1 2 1 1 2 0 0 0 0 1 2 1 1 2 0 2 0 2 2 2 2 2 0 0 0 0 1 0 0 2 1
 0]
  • predict_probaとdecision_functionの結果は(n_samples, n_classes)の形になるが,2クラス分類のdecision_funcsionの場合のみ陽性クラスであるclasses_[1]に対応する1列しかない
  • クラスの数だけ列がある場合は列に対してargmaxを計算すれば予測を再現できるが,クラスが文字列だったり,0から始まる整数で表現されていない場合には注意が必要
  • predictの結果をdecision_functionやpredict_probaの結果と比較する際にはclasses_属性を使って実際のクラス名を使うようにする
  • この例を次に示す
In[118]:
logreg = LogisticRegression()

named_target = iris.target_names[y_train]
logreg.fit(X_train, named_target)
print("unique classes in training data: {}".format(logreg.classes_))
print("predictions: {}".format(logreg.predict(X_test)[:10]))
argmax_dec_func = np.argmax(logreg.decision_function(X_test), axis=1)
print("argmax of decision function: {}".format(argmax_dec_func[:10]))
print("argmax combined with classes_: {}".format(logreg.classes_[argmax_dec_func][:10]))

Out[118]:
unique classes in training data: ['setosa' 'versicolor' 'virginica']  # 訓練データ中のクラス
predictions: ['versicolor' 'setosa' 'virginica' 'versicolor' 'versicolor' 'setosa'
 'versicolor' 'virginica' 'versicolor' 'versicolor']  # 予測値
argmax of decision function: [1 0 2 1 1 0 1 2 1 1]  # 決定関数のargmax
argmax combined with classes_: ['versicolor' 'setosa' 'virginica' 'versicolor' 'versicolor' 'setosa'
 'versicolor' 'virginica' 'versicolor' 'versicolor']  # 決定関数のargmaxをクラス名にしたもの
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python備忘録

高速化


range(0,10**4*1,1) range(0,10**4*2,2) range(0,10**4*3,3)
list 10.247 12.593 14.6
dict 0.253 0.028 0.028
set 0.239 0.024 0.022
  • 標準入力(input < sys.stdin.readline)
    • 標準入力はinput()よりsys.stdin.readline()の方が10倍速い
平均(ms) 標準偏差(ms)
input() 392.40 24.36
sys.stdin.readline() 37.09 1.88
  • 並び替え(sort > sorted)
    • ソートは、sortedよりsortの方が1.4倍速く、keyの指定にはitemgetterを使うとlambdaに比べ1.2倍速くなる
平均(ms) 標準偏差(ms)
sort() 88.54 56.98
sorted() 127.03 7.51
  • ループ(for > while)
    • ループは、forの方がwhileより2倍程度速い
平均(ms) 標準偏差(ms)
for _ in range(N) 20.63 0.89
for i in range(N) 25.66 0.93
while i < N 51.36 1.44
  • リスト初期化(* > in range)
    • リストの初期化は*演算子の方が、内包表記より8~9倍速い
平均(ms) 標準偏差(ms)
[None] * N 5.15 0.41
[None for _ in range(N)] 41.17 2.05
  • 二次元配列の初期化(* > in range)
    • 二次元配列の初期化は、最初の次元のみ内包表記を使う
平均(ms) 標準偏差(ms)
[[None] * N for _ in range(N)] 3.07 1.02
[[None for _ in range(N)] for _ in range(N)] 38.45 4.80
  • リストの値参照
    • リストの値の参照は、for a in Aのようにインデックスを作らないほうが、rangeを使う場合より4倍速い
平均(ms) 標準偏差(ms)
for i in range(len(A)) 41.14 0.56
for a in A 11.85 1.51
  • リストへの値追加
    • 要素の追加は内包表記を使うと、appendに比べ1.5倍速くなる
平均(ms) 標準偏差(ms)
append() 103.99 2.62
A[i] = i 70.97 3.93
[i for i in range(N)] 65.83 3.20

https://www.kumilog.net/entry/python-speed-comp

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

Joblibで共有メモリを設定する時につまづいたこと

Pythonで並列処理をしたい時、選択肢としてmultiprocessingかJoblibの二択がまず出てきますが、サクッとやりたい時はJoblibを使うことになると思います。

しかしプロセス毎に参照可能なメモリ領域が異なるため、並列実行している関数から外部スコープの変数に代入するためには共有メモリを設定しなければいけません。そこで以下の記事を参考にValueクラスを使って共有メモリを設定しようとしましたが、

[Python] Joblibでお手軽並列処理

なぜか動きません。試しに記事内に書かれているコードをそのままコピペしても、

# -*- coding: utf-8 -*-
from joblib import Parallel, delayed
from multiprocessing import Value, Array

shared_int = Value('i', 1)

def process(n):
    shared_int.value = 3.14
    return sum([i*n for i in range(100000)])

# 繰り返し計算 (並列化)
Parallel(n_jobs=-1)( [delayed(process)(i) for i in range(10000)] )

print(shared_int.value)
RuntimeError: Synchronized objects should only be shared between processes through inheritance

同様のエラーが起きちゃいます。さあどうしよう。どこも同じやり方しか書いてなくて困った。じゃあ公式リファレンスを見てみよう。

Embarrassingly parallel for loops

However if the parallel function really needs to rely on the shared memory semantics of threads, it should be made explicit with require='sharedmem', for instance:

>>> shared_set = set()
>>> def collect(x):
...    shared_set.add(x)
...
>>> Parallel(n_jobs=2, require='sharedmem')(
...     delayed(collect)(i) for i in range(5))
[None, None, None, None, None]
>>> sorted(shared_set)
[0, 1, 2, 3, 4]

えっ、Parallelの引数にrequire='sharedmem'設定するだけでいいんですか?試しにさっきのコードを書き換えてみると、

# -*- coding: utf-8 -*-
from joblib import Parallel, delayed

shared_int = 1

def process(n):
    global shared_int
    shared_int = 3.14
    return sum([i*n for i in range(10000)])

# 繰り返し計算 (並列化)
Parallel(n_jobs=-1, require='sharedmem')([delayed(process)(i) for i in range(10000)])

print(shared_int)
3.14

できました。ちゃんと並列化した関数の中から外部スコープの変数に代入できています。

てことで、どうやらParallelの引数にrequire='sharedmem'を入れるだけで共有メモリを設定できちゃうようです。簡単!

でも

Keep in mind that relying a on the shared-memory semantics is probably suboptimal from a performance point of view as concurrent access to a shared Python object will suffer from lock contention.

ーー和訳ーー
共有Pythonオブジェクトへの同時アクセスではロック競合が発生するため、パフォーマンスの観点からは、共有メモリーのセマンティクスに依存するのは最適とは言えないことに注意してください。

とも書いてあるので、気をつけて使ったほうが良さそうです。

参照

[Python] Joblibでお手軽並列処理

Embarrassingly parallel for loops

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

Pythonでの学習 その1

基礎を学ぶ

こんにちは!初心者です!!
学んだことをつらつら書いていきます!!

Aidemyでの学びとは

これはこんな意味ですよ〜
こう書きますよ〜
じゃあ!書いてみよう!

って感じでした。(これが結構大変)
写し書きと言われればそこまでですが、勉強の基本は真似をしてやってみることですよね!

では内容に入っていきたいと思います。

やっぱり最初は Hello World!!

main.py
print("Hello World")

>>
Hello World

はい。こんな感じです。
ここで疑問が発生します。

main.py
print('Hello World')

>>
Hello World

これでも出力は一緒なんですよ。
なんでなんですかねぇ。わからないまま次へ・・・

計算ができる!!

main.py
print(3 + 8)

>>
11

変数に入れちゃったり・・・

main.py
x = 3
y = 8
print(x + y)

>>
11

こんな書き方もできちゃいます!

main.py
x = 3
x += 8
print(x)

>>
11

使える演算子はこれ!

足し算:「+」
引き算:「-」 
掛け算:「
割り算:「/」
余りの計算:「%」
べき乗:「
*」

計算はなんでもできそう!!

文字の結合もできます!!

main.py
n = "ねこ"
m = "いぬ"
print(n + m)

>>
ねこいぬ

こんな使い方をしたり・・・ね?

main.py
n = "ねこ"
m = "いぬ"
print("私は" + n + "が好きで、" + m + "が嫌いです。")

>>
私はねこが好きで、いぬが嫌いです。

この辺はVBAと似てるな〜!とか思います。
でもここで落とし穴があってですね・・・。

main.py
n = "ねこ"
m = 2
print("私は" + n + "が好きで、" + m + "匹飼っています。")

>>
Error

これはエラーになるんですよね。(VBAだとすんなり行けた・・・はず・・・。)

これは型という概念があって、これがエラーの原因です。
str型:文字列
int型:整数
float型:浮動小数点

つまり上のエラーは
文字列の中に数値を入れるな!!
ってエラーみたいです。

型を合わせてあげるとうまくいきます。

main.py
n = "ねこ"
m = 2
print("私は" + n + "が好きで、" + str(m) + "匹飼っています。")

>>
私はねこが好きで、2匹飼っています。

型がどうしてもわからない!!!って時にはtype関数があるので使ってみてくださいね!

main.py
n = "ねこ"
m = 2
f = 0.12
print(type(n))
print(type(m))
print(type(f))

>>
<class 'str'>
<class 'int'>
<class 'float'>

長くなったので、今回はこの辺で!!

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

初心者向け Pythonの基本的な計算

概要

Pythonを始めたばかりの人向けに、基本的な計算の仕方や数値の書き方を書いていきます。

足し算

>>>1 + 1
2

引き算

>>>2 - 1
1

掛け算

掛け算は✖️ではなく「*」を使います。

>>>2 * 2
4

割り算

割り算は÷ではなく「/」を使います。

>>>4 / 2
2.0

割り算(小数切り捨て)

>>>5 // 2
2

割り算の余りを求める

>>>7 % 2
1

べき乗

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

Djangoのform機能を使ってBMI測定機を作成。我が体重への自戒を込めて。

どうもjackです。
8月からPythonをメインにエンジニア業務に従事することになりまして。
諸事情により12月までに7キロの減量を目標に生きてる駆け出しエンジニアです。
今回は表題の通りDjangoのform機能を使ってBMI測定を行います。

ちなみに私の開発環境は以下となっています。

  • Python 3.6.7
  • Django 2.2.3
  • macOS

では、早速。

django-admin startproject qiita

をターミナルにて実行してアプリを作成!

cd qiita

作成したディレクトリへ移動。

python3 manage.py startapp bmi

上記にてbmiアプリを作成。

qiitaディレクトリにあるsettings.pyのINSTALLED_APPSにbmiを追記。

settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'bmi',

qiitaディレクトリにあるurls.pyの編集を行います。

urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('bmi/', include('bmi.urls')),
]

合わせてbmiディレクトリにurls.pyのファイルを作成しておきましょう。
urls.pyの編集は後ほど。

qiitaディレクトリ
├── qiita
│   ├── __pycache__ 
│   ├── _init_.py
│   ├── setting.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── bmi
│   ├── migrations  
│   ├── _init_.py  
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py ←これ
│   └── views.py
└── manage.py

今回のbmiの計算に必要な数値は身長と体重の2つ。
この2つの値をformで受け取る為にまずはbmiディレクトリにforms.pyを作成。

qiitaディレクトリ
├── qiita
│   ├── __pycache__ 
│   ├── _init_.py
│   ├── setting.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── bmi
│   ├── migrations  
│   ├── _init_.py  
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py ←これ
│   ├── models.py
│   ├── tests.py
│   ├── urls.py 
│   └── views.py
└── manage.py

んで持ってこのforms.pyを編集します。

forms.py
from django import forms

class BmiForm(forms.Form):
    weight = forms.IntegerField(label='weight')
    height = forms.IntegerField(label = 'height')

ではこの作成したformをviewにて関数を定義していきます。

views.py
from django.shortcuts import render
from .forms import BmiForm 
# Create your views here.
def bmi(request):
    params = {
    'title' :'BMI',
    'msg': 'enter your score',
    'form': BmiForm()
    }

    if (request.method=='POST'):
        msg = int(request.POST['weight']) / (int(request.POST['height'])/100 )**2
        params['msg'] = '{:.2f}'.format(msg)
        params['form'] = BmiForm(request.POST)

    return render(request, 'bmi/bmi.html', params)

paramsの部分は後ほど作成するhtmlに値を渡すための部分になります。

入力フォームを埋め込むhtmlを格納するtemplatesディレクトリを作成します。

qiitaディレクトリ
├── qiita
│   ├── __pycache__ 
│   ├── _init_.py
│   ├── setting.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── bmi
│   ├── migrations  
│   ├── templates
│   │      └── bmi
│   │           └── bmi.html ←ここ
│   ├── _init_.py  
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py 
│   ├── models.py
│   ├── tests.py
│   ├── urls.py 
│   └── views.py
└── manage.py

そしてbmi.htmlを書いていきます。

bmi.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>{{title}}</title>
  </head>
  <body>
    <h1>{{title}}</h1>
    <p>{{msg}}</p>
    <table>
      <form action="{% url 'bmi' %}" method="post">
        {% csrf_token %}
        {{form}}
        <tr>
          <td></td><td><input type="submit" name="" value="send"></td>
        </tr>
      </form>
    </table>

  </body>
</html>

最後にurls.pyを編集して終了です。

urls.py
from django.urls import path, include
from . import views

urlpatterns = [
   path('', views.bmi, name='bmi'),
   ]

ターミナルを起動させてローカルサーバーを立ち上げましょう。

python3 manage.py runserver

スクリーンショット 2019-08-20 13.25.27.png

こういった表示が出れば完成です!!

実際に入力すると、、、

スクリーンショット 2019-08-20 13.27.17.png

でたーーーーー!!!!
ちなみに標準は22らしいです。泣

ではさよならさよならさよなら。

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

LeetCode / Linked List Cycle II

ブログ記事からの転載)

[https://leetcode.com/problems/linked-list-cycle-ii]

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.

Note: Do not modify the linked list.
スクリーンショット 2019-08-20 10.07.19.png
Follow-up:

Can you solve it without using extra space?

こちらの問題に続き、連結リストがお題。
この問題では、与えられた連結リストに循環構造(cycle)が含まれている場合、cycleが始まる地点を返す必要があります。
そしてこの問題でも、空間計算量を定数時間に抑えて行う方法を考えます。

解答・解説

解法1

空間計算量を定数時間に抑えるということで、ポインタを使うんだなということは想像がつきます。実際、今回も1ずつ進むslowと2ずつ進むfastのポインタを使います。しかし今回はcycleが始まる地点も返す必要があり、これをどうメモリを使わずに計算するかです。

これはなかなか自力で思いつきづらい気がしますが、下図のような状況を考えます。
図1_190820.png
slowとfastを同じ地点からスタートさせ(前の問題ではスタート地点を1ずらしていました)、次にslowとfastが出会うまでの間に、slowがH+D進み、fastはH+L+D進む状況を考えます。
すると、fastはslowの2倍の距離進んでいるので、2(H + D) = H + D + L の関係が成り立ちます。移項すれば、H = L - D の関係が成り立ちます。

求めたいcycleが始まる地点はHに相当しますが、H = L - D ということは、下図のようにslowをfastと出会った地点からスタートさせ、一方でheadというポインタを始点からスタートさせたとき、ちょうどcycleの始点で出会うことを意味します。
図2_190820.png
つまり、まずslowとfastを同じ地点からスタートさせ、slowとfastが出会ったら、そこからslowだけスタートさせ、同時に始点からheadをスタートさせれば、slowとheadが出会った地点が求めたい答えである、ということになります。

コードに落とすと、以下の通りとなります。

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        slow = fast = head
        # まず、slowとfastが再び出会うまで動かす
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow is fast:
                break
        else:
            return None
        # slowとheadが出会うまで動かす
        while head is not slow:
            slow = slow.next
            head = head.next
            print(head.val, slow.val)
        return head
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftでもPythonみたいに文字列操作したい!

最近使い始めたSwiftでもPythonみたいに文字列操作したい!

SwiftでもPythonみたいに文字列のスライスしたいなぁ

ってことでStringを拡張してPythonの文字列操作メソッドを生やすライブラリを作ってみました。

SwiftyPyString

https://github.com/ChanTsune/SwiftyPyString

Swift5で動きます。
Unicode準拠 & Pythonのドキュメント準拠で作ってます。

インストール

CocoaPods、Carthage、SwiftPMの主要なパッケージマネージャ3つに対応させています。

CocoaPods

pod 'SwiftyPyString'

Carthage

github 'ChanTsune/SwiftyPyString'

SwiftPM

import PackageDescription

let package = Package(
    name: "YourProject",
    dependencies: [
        .Package(url: "https://github.com/ChanTsune/SwiftyPyString.git", from: "1.0.1")
    ]
)

文字列操作

添字アクセス

let str = "0123456789"
str[0]
// 0
str[-1]
// 9

Python準拠なので負の数を利用した後ろからのアクセスもサポートしています。

文字列のスライス

let str = "0123456789"
str[0,5]
// 01234
str[0,8,2]
// 0246
str[nil,nil,-1]
// 9876543210

やりたかったスライスです。

一度これに慣れると他の言語でも使いたくなるやつです。

(だからこれを作ったわけですが笑)

コロンだけの省略記法はnilで代用することにしました。

ちなみに同じ動作をPythonで書くと以下のようになります。

str = "0123456789"
str[0:5]
# 01234
str[0:8:2]
# 0246
str[::-1]
# 9876543210

文字列検索

// 先頭からの検索  
"123412312312345".find("123") // 0

// 開始位置を指定して検索
"123412312312345".find("123",start:2) // 4

// 終了位置を指定して検索
"123412312312345".find("123",end:1) // -1

// 末尾からの検索
"123412312312345".rfind("123") // 10

末尾からの検索も同様に開始位置と終了位置を指定して検索できます。

文字列結合

let array = ["abc","def","ghi"]
"".join(array) // "abcdefghi"
"-".join(array) // "abc-def-ghi"
"++".join(array) // "abc++def++ghi"

トリミング

// 右端のみ
"rstrip sample   ".rstrip() // "rstrip sample"
"rstrip sample   ".rstrip("sample ") // "rstri"
"  rstrip sample".rstrip() // "  rstrip sample"

// 左端のみ
"  lstrip sample".lstrip() // "lstrip sample"
"  lstrip sample".lstrip(" ls") // "trip sample"
"lstrip sample".lstrip() // "lstrip sample"

// 両端
"   spacious   ".strip() // "spacious"
"www.example.com".strip("cmowz.") // "example"

文字列分割

行ごとの分割
"abc\nabc".splitlines() // ["abc", "abc"]
"abc\r\nabc\n".splitlines() // ["abc", "abc"]

// 改行文字を残して分割
"abc\nabc\r".splitlines(true) // ["abc\n", "abc\r"]
"abc\r\nabc\n".splitlines(true) // ["abc\r\n", "abc\n"]
指定文字での分割
"a,b,c,d,".split(",") // ["a", "b", "c", "d", ""]

"aabbxxaabbaaddbb".split("aa") // ["", "bbxx", "bb", "ddbb"]

// 分割の回数を指定
"a,b,c,d,".split(",", maxsplit: 2) // ["a", "b", "c,d,"]

出現回数カウント

"abc abc abc".count("abc") // 3

// 開始位置の指定
"abc abc abc".count("abc", start:2) // 2

// 終了位置の指定
"abc abc abc".count("abc", end:1) // 0

ゼロ埋め

"abc".zfill(1) // "abc"
"abc".zfill(5) // "00abc"

// 符号付きの場合
"+12".zfill(5) // "+0012"
"-3".zfill(5) // "-0003"
"+12".zfill(2) // "+12"

符号付きの場合は符号の後ろにゼロが入ります。

さいごに

以上、簡単に主要な機能の説明をさせて頂きました。

紹介したメソッド以外にもPython3.7.3の時点で利用できるstr型のメソッドは言語機能的に実装出来ない、あるいは実装が難しいもの以外はほとんど実装してあります。

実装してあるメソッドの一覧は、こちらをご覧ください。
https://github.com/ChanTsune/SwiftyPyString/blob/master/README.md

一部、標準のメソッドと機能がかぶるものもありますが、PythonからSwiftに移植したいなんて言う事があれば多少は移植作業が楽になるのではないでしょうか?(普通にPythonを動くようにした方が多分楽 笑)

そうでなくともPythonからプログラミングを始めたという人なら、慣れ親しんだPythonの文字列操作ができるようになるので比較的便利ではないでしょうか?

このメソッド実装できるよ、とかこっちの実装の方がパフォーマンスいいんじゃない? Swiftだったらこう書くと綺麗だよ等ありましたら教えてください。

プルリクお待ちしております。

もしあれば、バグ報告とかも嬉しいです。

以前、C++版も作っているのでこちらも宜しければ
c++でもpythonのstr型のメソッドを使いたい!
https://qiita.com/ChanTsune/items/38814ca81738877c51fe

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

DjangoでSuperUserだけが機能を使えるようにする方法

はじめに

未来電子テクノロジーでインターンをしています、Sotaです。
プログラミング初心者であるため、内容に誤りがあるかもしれません。
もし、誤りがあれば修正するのでどんどん指摘してください。

今回はDjangoで管理者だけに機能を操作できる権限を与える方法について書いていきます。

@staff_member_required

通常、ログインしているユーザーだけに機能を操作する権限を与えるには、

from django.contrib.auth.decorators import login_required

@login_required
def hogehoge(request):
    ...

とします。

これをcreatesuperuserで作成した管理者(SuperUser)だけが操作できるようにするには、

from django.contrib.admin.views.decorators import staff_member_required

@staff_member_required
def hogehoge(request):
    ...

とします。
使い方はほとんど同じで、分かりやすかったです。

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

Donkey Carを組み立てる前にシミュレーターで楽しんでみる Donkey Car 3.1.0編

はじめに

Donkey Carは市販のラジコンカーを改造して自律走行させることができるプラットフォームです。RCカーの改造の仕方や機械学習アルゴリズムがオープンソースで公開されいるので誰でも組み立てることができ、各地で走行会やレースが開催されています。

AIカーが来てる! 自動運転でラジコンカーを走らせよう!

先日のMaker Faire Tokyo 2019でもAIでRCカーを走らせよう!コミュニティの主宰で走行会&レースが開催されました。

Donkey CarはRCカーにRaspberry Piなどの小型コンピュータとカメラを搭載してカメラで撮影した画像を機械学習アルゴリズムで認識し、RCカーのステアリングやスロットルをコンピュータ制御する仕組みになっています。搭載するコンピュータはRaspberry Pi 3b+の他、バージョン3.0.2からNVIDIA Jetson Nanoに対応しました。

この記事では、Donkey Carに関心を持たれた方々向けに、Donkey Simulatorを使ってPC上でDonkey Carのテスト走行、教師あり学習、自律走行をシミュレーションで楽しむ方法をご紹介します。

RCカーを購入する前にDonkey Carで必要な作業がどんなものなのか、どういったところが楽しいのかを体感していただければ幸いです。

以下、Macによる手順をご紹介しますがWindowsでもほぼ同じ手順で実現可能です。Windowsでセットアップする場合は、Anaconda NavigatorをインストールしてPythonの仮想環境を作成してから下記を実行すると便利だと思います。

Donkey Simulatorをインストールする

donkey_sim_icon.png

シミュレーターをインストールします。各プラットフォーム毎に用意されています
https://github.com/tawnkramer/gym-donkeycar/releases

Macの場合、最新版をダウンロードしてzipで展開するとDonkeySimMacが作成されます。その中にあるdonkey_sim/Applicationにドラッグ&ドロップします。このようにセットアップしたDonkey Simulatorのロードモジュールのパスは以下のようになります

/Applications/donkey_sim.app/Contents/MacOS/donkey_sim

これのパスはあとで大事になるので覚えておいてください

DonkeyCarをセットアップする

Install Donkeycar on Macに従ってPCにdonkeycarをセットアップします。Donkey Carは現時点の最新バージョン3.1.0です。Donkey Carの開発は活発なのでバージョンアップされてセットアップ方法が変更される可能性があります

以下の環境を想定しています

Mac 10.14.5
Python 3.7.3

まず適当なディレクトリを作成します。ディレクトリ名はなんでもいいですが、ここではdonkeypjtestとしました

mkdir ./donkeypjtest
cd donkeypjtest

tensorflowをインストールします。tensorflowでGPUが利用可能な環境であればtensorflow-gpuをインストールすると高速に学習できます

pip install tensorflow

donkeycarのレポジトリをcloneしてセットアップします

git clone -b master https://github.com/autorope/donkeycar
pip install -e donkeycar

上記が正常にセットアップできると、donkeyコマンドが使えるようになります

gym-donkeycarをセットアップします

git clone https://github.com/tawnkramer/gym-donkeycar
pip install -e gym-donkeycar

プロジェクトを作成する

donkey createcarコマンドでプロジェクトを作成します

donkey createcar --path ./mycar
cd ./mycar

プロジェクトが作成されると以下のようなディレクトリ構成になります

tree .
.
├── config.py
├── data
├── logs
├── manage.py
├── models
├── myconfig.py
└── train.py

3 directories, 4 files

キモはmanage.pyです。基本的にpython manage.py trainpython manage.py driveという2つのコマンドを使います

python manage.py 
using donkey v3.1.0 ...
Usage:
    manage.py (drive) [--model=<model>] [--js] [--type=(linear|categorical|rnn|imu|behavior|3d|localizer|latent)] [--camera=(single|stereo)] [--meta=<key:value> ...]
    manage.py (train) [--tub=<tub1,tub2,..tubn>] [--file=<file> ...] (--model=<model>) [--transfer=<model>] [--type=(linear|categorical|rnn|imu|behavior|3d|localizer)] [--continuous] [--aug]

myconfig.pyを編集します。このプロジェクトをDonkey Simulatorに対応させるための設定を行います。200行目あたりの以下の3行のコメントを外し、DONKEY_GYMTrueDONKEY_SIM_PATHにDonkey Simulatorのパスを設定します

myconfig.py
DONKEY_GYM = True
DONKEY_SIM_PATH = "/Applications/donkey_sim.app/Contents/MacOS/donkey_sim"
DONKEY_GYM_ENV_NAME = "donkey-generated-track-v0"

以上でDonkey Carシミュレーターで遊ぶ準備ができました

シミュレーターを起動してテスト走行する

donkey_simを起動します。コンフィギュレーションダイアログが表示されるのでScreen resolution800 x 600Windowedにチェックを入れてPlay!ボタンをクリックします。

Screen resolutionはなんでもよいです。ディスプレイの解像度に合わせて指定してください。Windowedのチェックを外すとフルスクリーンモードで起動します

スクリーンショット 2019-08-19 11.43.26.png

シミュレーターには複数のコースが用意されています。今回は左下のGenerated Trackをクリックします

スクリーンショット 2019-08-19 11.46.30.png

Joystick/Keyboard No Recをクリックするとコースを走ることができます。キーボードのカーソルキーで前進で左折で右折でバックです。Stopボタンをクリックするとコースを終了します

スクリーンショット 2019-08-19 11.52.46.png

しばらく走行してみて感覚をつかんでください。このあと教師用データを集めるために走行しますが、その前にキーボードでの走行に慣れておくほうがよいです

教師用データを取得する

Donkey Car実機では教師データを取得するために手動で記録用走行を行います。このとき前方に搭載したカメラで走行中の景色を画像データとして記録し、同時に手動操作されたスロットルやハンドルの操作履歴を記録してそれを教師用データとします。シミュレーターでは記録モードで走行することで実機と同様のデータを取得することができます。

まず、テスト走行のときと同様にdonkey_simを起動します。コースを選択する前に画面右下のLog dirボタンをクリックします

スクリーンショット 2019-08-19 16.13.25.png

ダイアログで先ほど作成したmycarプロジェクトのlogsフォルダを指定してSelectボタンをクリックします

スクリーンショット 2019-08-19 16.18.44.png

Generated Trackを選択します

スクリーンショット 2019-08-19 11.46.30.png

Joystick/Keyboard w Recボタンをクリックして記録用走行を開始します

スクリーンショット 2019-08-19 16.22.26.png

カーソルキーでDonkey Carを操作してコースを周回します。左下のLog:の表示で記録されている画像の枚数を確認できます。だいたい5000〜10000くらいを目安にしてください。十分に記録ができたら右上のStopボタンでシミュレーターを終了します

スクリーンショット 2019-08-19 16.24.43.png

記録走行が終了すると、./mycar/logsに教師用データが保存されます

スクリーンショット 2019-08-19 16.31.15.png

カメラ画像は160x120ピクセルのJPEG画像で保存されています

550_cam-image_array_.jpg

スロットルやハンドル確度などの情報がJSONで保存されています。保存されているJSONデータは改行されていませんが、下記は読みやすさのため改行して記載しています

record_550.json
{
  "cam/image_array":"550_cam-image_array_.jpg",
  "user/throttle":10.0,
  "user/angle":0.0,
  "user/mode":"user",
  "track/lap":0,
  "track/loc":10
}

学習する

mycarプロジェクト下で以下のコマンドを実行して学習を開始します。ここで--type=categoricalの指定は大事なので覚えておいてください

python manage.py train --tub=logs --model=models/mypilot.h5 --type=categorical

以下のように./mycar/logsのデータでモデルを訓練していきます。訓練が終わるまで待ちます

using donkey v3.1.0 ...
loading config file: /src/work/donkeypjtest/mycar/config.py
loading personal config over-rides

config loaded

...中略...

found 0 pickles writing json records and images in tub logs
logs
collating 7538 records ...
train: 6030, val: 1508
total records: 7538
steps_per_epoch 47
Epoch 1/100
46/47 [============================>.] - ETA: 1s - loss: 2.0719 - angle_out_loss: 1.9795 - throttle_out_loss: 1.0821 - angle_out_acc: 0.4536 - throttle_out_acc: 0.5652      
Epoch 00001: val_loss improved from inf to 1.35425, saving model to models/mypilot.h5
47/47 [==============================] - 83s 2s/step - loss: 2.0617 - angle_out_loss: 1.9745 - throttle_out_loss: 1.0744 - angle_out_acc: 0.4543 - throttle_out_acc: 0.5662 - val_loss: 1.3542 - val_angle_out_loss: 1.4905 - val_throttle_out_loss: 0.6090 - val_angle_out_acc: 0.5817 - val_throttle_out_acc: 0.6513
Epoch 2/100
17/47 [=========>....................] - ETA: 47s - loss: 1.3048 - angle_out_loss: 1.5335 - throttle_out_loss: 0.5380 - angle_out_acc: 0.6140 - throttle_out_acc: 0.7403 

学習が終わると./mycar/models下にmypilot.h5という学習済モデルが出来上がります

スクリーンショット 2019-08-19 17.12.00.png

自律走行してみる

以下のコマンドを実行します。--type=categoricalに注意してください。--typeで指定するモデルタイプは上記のpython manage.py trainのときに指定したものと必ず一致させる必要があります

python manage.py drive --type=categorical --model=models/mypilot.h5 

上記を実行するとDonkey Simulatorのコンフィギュレーションダイアログが起動するのでテスト走行のときのように設定してPlay!をクリックします

スクリーンショット 2019-08-19 11.43.26.png

シミュレーターが起動してしばらくするとターミナルに以下のようなログが表示されます

finished loading in 3.236067771911621 sec.
Adding part FileWatcher.
Adding part FileWatcher.
Adding part DelayedTrigger.
Adding part TriggeredCallback.
Adding part KerasCategorical.
Adding part DriveMode.
Adding part AiLaunch.
Adding part AiRunCondition.
Tub does NOT exist. Creating new tub...
New tub created at: /src/work/donkeypjtest/mycar/data/tub_1_19-08-19
Adding part TubWriter.
You can now go to <your pi ip address>:8887 to drive your car.
Starting vehicle...
8887

Starting vehicle...と表示されたら、Webブラウザで http://localhost:8887 にアクセスします。すると以下のようなDonkey Monitorが表示されます

スクリーンショット 2019-08-19 22.40.40.png

画面右下のMode & PilotのセレクトボックスからLocal Pilot(d)を選択すると、シミュレーター上のDonkey Carが動き出します

スクリーンショット 2019-08-19 22.46.08.png

これが先ほど訓練した学習済みモデルmypilot.h5を使った自律運転です。学習が不十分だった場合はコースをうまく認識せずにコースアウトしたり、スタート地点から動かないことすらあります。そういったときには右下のExitボタンをクリックするとコースが再起動して運転を再開します

同じ学習済みモデルを使っていても一回ごとにDonkey Carの運転は微妙に違いますし、新たに訓練したモデルを使うとまた違う動きをします。記録する画像の量も多ければよい運転をするとも限りません。記録走行での運転方法や画像の枚数などをいろいろ変えてみてうまくコースが周回できるモデルを作成してみてください

記録、訓練、自律走行までの一連の流れはDonkey Car実機でもほぼ同じです。シミュレーターでこの流れに慣れておけば実機を組み立てた際にスムーズに走らせることができるでしょう

シミュレーターの遊び方は以上です

遊ぶ上でのポイントとかコツとか

上記に慣れた後に楽しめるポイントやぼくが経験上得たコツなんかを記載しておきます。参考になれば幸いです

Colabで高速に訓練する

FaBo DonkeyCar Docs Colabでの学習(GPU)Google ColabのGPUを利用できるnotebookが公開されています。このnotebookを使うとGPUを使って高速に訓練することができるので大変便利です。教師用画像データが5000枚程度であればだいたい5分くらいで訓練が完了します。使い方はNotebookに丁寧に記載されているので、記載どおりにすすめていけばよいです

いろんなモデルを訓練してみる

python manage.py train --type=[MODEL TYPE]で指定できるモデルタイプは複数あり、様々なアルゴリズムを使って訓練させることができます。現時点でサポートされているタイプはlinear|categorical|rnn|imu|behavior|3d|localizerとなっています。各モデルの解説は公式ドキュメントのKerasに記載されています

同じ教師データでも異なるモデルを訓練させることで全く違う運転をするのでいろいろ試してみてください。経験上categoricalが安定している気がしています

上述のとおり、訓練時と運転時の--type指定は同一にする必要があります。異なるモデルを指定した場合はpython manage.py driveでのモデル読み込み時にエラーが発生します

何度訓練してもスタート地点から発進しない場合

何度記録と訓練を繰り返してもDonkey Carがスタート地点から発進しないことがありました。いろいろ試したところ記録運転で周回しているときにスタート地点周辺でスロットルをオン(キー)していると発進しやすくなりました

検証したわけではないですが、おそらくスタート地点周辺でスロットルを開けてるデータを学習させないと発進時にスロットルを開ける確率が低下してしまうためではないかと思います。スタート地点前後ではキーを押し続けるとか小刻みに連打するなどして周辺でスロットルを開けることを教えてやるといいでしょう

スクリーンショット 2019-08-19 16.25.36.png

おわりに

Donkey Carでシミュレーターを使った遊び方を紹介しました。Donkey Carは実機を組み立てて現実世界のコースで走らせるのがなんといっても楽しいですが、組み立てる前の練習や訓練方法を試行するときなどにシミュレーターが役に立ちますし、シミュレーターであれこれ試行錯誤するだけでもなかなか楽しいものです。

Donkey Carに興味を持たれた方はぜひシミュレーターを使ってDonkey Carに対する知識を深めてください!

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

UWSC を Python で置換しよう(2)関数置き換え

はじめに

「UWSC を Python で置換しよう」第二回です。今回は低レベル操作系の置き換えをしていこうとおもいます。
とりあえず、方向性として、サンプルスクリプトを用意するか、UWSCの関数をPythonで再現するか悩んだ結果、UWSCの関数をPythonで再現する方向でやってみようかと。

※要はUWSCスクリプトを動かせるラッパーを目指したのですが、結論から言ってしまうと、UWSCとは似ても似つかない中途半端なものができてしまったので、次回は、大人しく、UWSCスクリプトの動作をPythonで再現する方向にしようと思います。取り敢えず、微妙なものになりましたが、どうなったのか気になる人もいる(?)とおもうので公開しておきます。

今回の対象はUWSCの関数

G_SCREEN_ , G_MOUSE_ , GET_WIN_DIR , GET_SYS_DIR , GET_CUR_DIR
ASC , CHR , MMV , BTN , KBD , CHKIMG , MSGBOX , INPUT , SAVEIMG 
  • 使用モジュール
head.py
# -*- coding: utf-8 -*-
import os,sys,re,time,subprocess
import pyautogui
import win32com.client

置き換え関数

早速、PythonでUWSCの関数を作っていく

  • G_SCREEN_W(画面幅)
def G_SCREEN_W():
    screen_x,screen_y = pyautogui.size()
    return screen_x
  • G_SCREEN_H(画面高)
def G_SCREEN_H():
    screen_x,screen_y = pyautogui.size()
    return screen_y
  • G_SCREEN_C(画面色数)
def G_SCREEN_C():
    im  = pyautogui.screenshot()
    colordepth = im.dtype
    return colordepth
  • GET_WIN_DIR(WindowsのPATH)
def GET_WIN_DIR():
    return os.environ['windir']
  • GET_SYS_DIR(System32のPATH)
def GET_SYS_DIR():
    sysdir = os.environ['SystemRoot'] + '¥System32'
    return sysdir
  • GET_CUR_DIR(カレントPATH)
def GET_CUR_DIR():
    return os.getcwd()
  • ASC(文字コード変換)
def ASC(str):
    return str.encode('UTF-8')
  • CHR(文字コード変換)
def CHR(str):
    return str.encode('ASCII')
  • MMV(マウスカーソル移動)
# MMV(X,Y,ms)
def MMV(x,y,deley):
    msec = deley / 1000
    time.sleep(msec)
    pyautogui.moveTo(x,y)
  • BTN(マウス操作)
def BTN(key,sta,x,y,deley):
    msec = deley / 1000
    time.sleep(msec)
    if sta == 0:
        if key == 'LEFT':
            pyautogui.click(x, y)
        if key == 'RIGHT':
            pyautogui.rightClick(x, y)
        if key == 'MIDDLE':
            pyautogui.middleClick(x, y)
        if key == 'WHEEL':
            pyautogui.scroll(sta, x, y)
        if key == 'TOUCH':
            pyautogui.click(x,y)
    if sta == 1:
        if key == 'LEFT':
            pyautogui.mouseDown(x, y, button='left')
        if key == 'RIGHT':
            pyautogui.mouseDown(x, y, button='right')
        if key == 'MIDDLE':
            pyautogui.mouseDown(x, y, button='middle')
        if key == 'WHEEL':
            pyautogui.scroll(sta, x, y)
        if key == 'TOUCH':
            pyautogui.mouseDown(x, y, button='left')
    if sta == 2:
        if key == 'LEFT':
            pyautogui.mouseUp(x, y, button='left')
        if key == 'RIGHT':
            pyautogui.mouseUp(x, y, button='right')
        if key == 'MIDDLE':
            pyautogui.mouseUp(x, y, button='middle')
        if key == 'WHEEL':
            pyautogui.scroll(sta, x, y)
        if key == 'TOUCH':
            pyautogui.mouseUp(x, y, button='left')
    if sta > 2:
        pyautogui.scroll(sta, x, y)
    if sta < 0:
        pyautogui.scroll(sta, x, y)
  • KBD (キー操作)
def KBD(key,sta,deley):
    msec = deley / 1000
    time.sleep(msec)
    if sta == 0:
        pyautogui.press(key)
    if sta == 1:
        pyautogui.keyDown(key)
    if sta == 2:
        pyautogui.keyUp(key)
  • CHKIMG (画像マッチング)
def CHKIMG(filename,maskcolor,rx1,ry1,rx2,ry2,num,conf):
    #返値はtuple型
    if len(conf) > 11:
        CFG = 0.95
    else:
        CFG = 0.98
    counter = 0        
    for pos in pyautogui.locateAllOnScreen(filename,region=(rx1,ry1,rx2,ry2),confidence=CFG):
        lpos = pos
        if counter == num:
            break
        counter += 1
    return lpos
  • MSGBOX (メッセージボックス)
def MSGBOX(mes,btype):
    if btype == 'BTN_OK':
        result = pyautogui.alert(text=mes,title='',button='OK')
    if btype == 'BTN_NO':
        result = pyautogui.alert(text=mes,title='',button='NO')
    if btype == 'BTN_YES':
        result = pyautogui.confirm(text=mes, title='', buttons=['Yes', 'No'])
    if btype == 'BTN_CANCEL':
        result = pyautogui.confirm(text=mes, title='', buttons=['OK', 'Cancel'])
    if btype == 'BTN_ABORT':
        result = pyautogui.alert(text=mes,title='',button='ABORT')
    if btype == 'BTN_RETRY':
        result = pyautogui.alert(text=mes,title='',button='RETRY')
    if btype == 'BTN_IGNORE':
        result = pyautogui.alert(text=mes,title='',button='IGNORE')
    return result
  • INPUT (入力ボックス)
def INPUT(mes,defv,hide):
    if hide == 0:
        result = pyautogui.prompt(text=mes, title='',default=defv)
    if hide == 1:
        result = pyautogui.password(text=mes, title='', default=defv, mask='*')
    return result
  • SAVEIMG (スクリーンショット)
def SAVEIMAGE(filename,x1,y1,x2,y2):
    pyautogui.screenshot(filename,region=(x1,y1,x2,y2))

使ってみる

とりあえす、上記の関数を収めた、 compatible_uwsc.pyを作った
で、 uwsc_demo.py というデモ用のファイルを作り、UWSCっぽいスクリプトを書く

# -*- coding: utf-8 -*-

import compatible_uwsc as UWSC

VAL1 = UWSC.INPUT('Please Input Word','Sample...',0)
UWSC.MSGBOX(VAL1,'BTN_OK')

UWSC.SAVEIMAGE('screenshot.png',1,1,1920,1080)

UWSC.MMV(10,15,1000)
UWSC.MMV(100,150,1000)
UWSC.MMV(10,150,1000)
UWSC.MMV(100,15,1000)
UWSC.MMV(50,150,1000)

UWSC.BTN('LEFT',0,100,100,1000)
UWSC.BTN('RIGHT',0,100,100,1000)

UWSC.KBD('ctrl',1,1000)
UWSC.KBD('a',0,1000)
UWSC.KBD('c',0,1000)
UWSC.KBD('ctrl',2,1000)

これだとPython的になってしまう…できればUWSCのスクリプトをそのまま動かせないかな…

さいごに

ラッパーを作る方向で関数を作ると、UWSCをやってた人にはかろうじてわかる...かもしれないものができたが、これじゃない感がすごい。この方向は、スキルがないと私程度の知識では中途半端なものしかできそうにないので、失敗ですかね。
というわけで、次回は大人しく、UWSCで作ったスクリプトやUWSC関数の動作をPythonで再現していく方向にしようと思います。

次回は UWSC を Python で置換しよう(3) Pythonで動作を再現 の予定

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

Pythonで始める機械学習 ニューラルネットワーク

ニューラルネットワーク

  • ディープラーニングと呼ばれる
  • ディープラーニングを理解する入り口として多層パーセプトロン(MLP)によるクラス分類と回帰がある
  • また多層パーセプトロンもニューラルネットワークと呼ばれる

ニューラルネットワーク

  • MLPは線形モデルを一般化し,決定までに複数のステージで計算するものとみることができる
  • 線形モデルの式はy = w[0] × x[0] + w[1] × x[1] + ・・・w[p] × x[p] + bで表されこれを言葉で言い換えると,yは入力特徴量x[0]からx[p]までの重み付き和で,重みは学習された係数w[0]からw[p]までで与えられる
  • これを図で表したものが次のものである
In[87]:
display(mglearn.plots.plot_logistic_regression_graph())

Out[87]:
db40f50ea12dba181cf5793bb8eaa648.png

  • 左のノードは入力特徴量,接続している線が学習された係数,右のノードは出力を表す
  • 出力は入力に対する重み付き和となっている
  • 次にMLPの計算を図で表す
In[88]:
display(mglearn.plots.plot_single_hidden_layer_graph())

Out[88]:
415062674e846c9a305613be0bcc5f87.png

  • 中間処理の隠れユニット(hidden layer)の計算で重み付き和が行われ,次に隠れユニットの値に対しての重み付き和が行われ最後の結果が表示される
  • この矢印が重み(係数)
  • このモデルをより強力にするには,隠れユニットの重み付き和を計算した結果に対して非線形関数(relu関数,tanh)を適用する
  • reluはゼロ以下の値を切り捨てる
  • tanhは小さい値に対しては-1に,大きい値に対しては+1に飽和する
  • これらの関数を次の図に示す
In[89]:
line = np.linspace(-3, 3, 100)
plt.plot(line, np.tanh(line), label="tanh")
plt.plot(line, np.maximum(line, 0), label="relu")
plt.legend(loc="best")
plt.xlabel("x")
plt.ylabel("relu(x), tanh(x)")

Out[89]:
ダウンロード (32).png

  • Out[87]の隠れユニットのノード数はパラメータ設定により調整することができる
  • 隠れノード数は小さく単純なデータで10くらい,大きく複雑なデータでは10,000にもなる
  • 次の図のように隠れ層を追加することもできる
In[90]:
mglearn.plots.plot_two_hidden_layer_graph()

Out[90]:
57e2107e844897127a433a8f5f88cf6d.png

  • このような計算層をたくさん持つニューラルネットからディープラーニングという言葉が生まれた

ニューラルネットワークのチューニング

  • two_moonsデータセットにMLPClassifierを適用してMLPの動作を見てみる
In[91]:
from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=100, noise=0.25, random_state=3)

X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)
mlp = MLPClassifier(solver='lbfgs', random_state=0).fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")

Out[91]:
ダウンロード (33).png

  • MLPの隠れユニット数はデフォルトで100個である
  • この小さいデータセットに対して100個の隠れユニットは大きすぎるため,隠れユニットの数を減らしモデルを単純にすることで精度を上げる
In[92]:
mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[10])
mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")

Out[92]:
ダウンロード (36).png

  • 隠れユニット数を10個に設定しているため決定境界はギザギザになっている
  • 隠れ層が1層の場合にはrelu関数を使うと決定境界は10個の線分からできたギザギザのものになる
  • 決定境界を滑らかにするためには隠れユニット数を増やす,隠れ層を増やす,tanhを用いるなどの方法がある
  • 上記の方法を用いて決定境界を滑らかにした例を見てみる
In[93]:
# 10ユニットの隠れ層を2つ使ったもの
mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[10, 10])

mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")

In[94]:
# 10ユニットの隠れ層を2つ使い,tanh関数を使う
mlp = MLPClassifier(solver='lbfgs', activation='tanh', random_state=0, hidden_layer_sizes=[10, 10])

mlp.fit(X_train, y_train)
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")

Out[93]:
ダウンロード (37).png

Out[94]:
ダウンロード (38).png

  • 隠れ層の追加に加えてtanh関数を用いたモデルの方が決定境界は滑らかになる
  • モデルの複雑さを制御するには,リッジ回帰や線形クラス分類で行ったようにl2ペナルティで重みを0に近づけることでも可能である
  • MLPでのl2は,線形回帰モデルと同じようにalphaパラメータの調整することで正則化を行う
  • alphaはデフォルトでは弱い正則化であり,値が増加するにつれて正則化が強まる
  • 次の例では2層の隠れ層をもつ10ユニット,100ユニットの例を見てみる
In[95]:
fig, axes = plt.subplots(2, 4, figsize=(20, 8))
for axx, n_hidden_nodes in zip(axes, [10, 100]):
    for ax, alpha in zip(axx, [0.0001, 0.01, 0.1, 1]):
        mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[n_hidden_nodes, n_hidden_nodes], alpha=alpha)
        mlp.fit(X_train, y_train)
        mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
        mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
        ax.set_title("n_hidden=[{}, {}]\nalpha={:.4f}".format(n_hidden_nodes, n_hidden_nodes, alpha))

Out[95]:
ダウンロード (39).png

  • ニューラルネットワークの複雑さを制御する方法として,「隠れ層」「隠れユニット数」「alphaパラメータ」があることがわかる
  • ニューラルネットワークではパラメータが同じでも異なる乱数シードを用いると異なったモデルになる
  • 次にこの例を見てみる
In[96]:
fig, axes = plt.subplots(2, 4,figsize=(20, 8))
for i, ax in enumerate(axes.ravel()):
    mlp = MLPClassifier(solver='lbfgs', random_state=i, hidden_layer_sizes=[100, 100])
    mlp.fit(X_train, y_train)
    mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)

Out[96]:
ダウンロード (40).png

  • 次にcancerデータセットにMLPClassifierを適用する
In[97]:  # 格特徴量の最大値を取得する
print("Cancer data per-feature maxima:\n{}".format(cancer.data.max(axis=0)))

In[98]:
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)

mlp = MLPClassifier(random_state=42)
mlp.fit(X_train, y_train)

print("Accuracy on training set: {:.3f}".format(mlp.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test, y_test)))

Out[97]:
Cancer data per-feature maxima:
[2.811e+01 3.928e+01 1.885e+02 2.501e+03 1.634e-01 3.454e-01 4.268e-01
 2.012e-01 3.040e-01 9.744e-02 2.873e+00 4.885e+00 2.198e+01 5.422e+02
 3.113e-02 1.354e-01 3.960e-01 5.279e-02 7.895e-02 2.984e-02 3.604e+01
 4.954e+01 2.512e+02 4.254e+03 2.226e-01 1.058e+00 1.252e+00 2.910e-01
 6.638e-01 2.075e-01]

Out[98]:
Accuracy on training set: 0.939
Accuracy on test set: 0.916

  • MLPの精度はかなり良いが,cancerのデータスケールの問題により他のモデル程の精度ではない
  • データスケールは平均が0,分散で1であることが理想である
  • 次にスケールの変換を行う
In[99]:
# 訓練セットの特徴量ごとの平均値を算出
mean_on_train = X_train.mean(axis=0)
# 訓練セットの特徴量ごとの標準偏差を算出
std_on_train = X_train.std(axis=0)

# 平均を引き,標準偏差の逆数でスケール変換する これでmean=0,std=1となる
X_train_scaled = (X_train - mean_on_train) / std_on_train
# テストセットも同様に行う
X_test_scaled = (X_test - mean_on_train) / std_on_train

mlp = MLPClassifier(random_state=0)
mlp.fit(X_train_scaled, y_train)

print("Accuracy on training set: {:.3f}".format(mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))

Out[99]:
Accuracy on training set: 0.991
Accuracy on test set: 0.965
  • スケール変換を行うことで性能が向上した
  • ここで学習の繰り返し回数の指定を行うことででモデルの調整を行うことができる
In[100]:
mlp = MLPClassifier(max_iter=1000, random_state=0)
mlp.fit(X_train_scaled, y_train)

print("Accuracy on training set: {:.3f}".format(mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))

Out[100]:
Accuracy on training set: 0.998
Accuracy on test set: 0.972
  • 学習の繰り返し回数を指定することでモデルの性能が向上した
  • alphaパラメータ調整による正則化の強化でもモデルの性能を向上できる
  • ニューラルネットワークが学習した内容を可視化することは難しい
  • 学習した内容をみる方法として,モデル内部の重みを可視化することが挙げられる
  • これをcancerデータセットに対して試す
In[102]:
plt.figure(figsize=(20, 5))
plt.imshow(mlp.coefs_[0], interpolation='none', cmap='viridis')
plt.yticks(range(30), cancer.feature_names)
plt.xlabel("Columns in weight matrix")
plt.ylabel("Input feature")
plt.colorbar()

Out[102]:
ダウンロード (41).png

  • 入力と第一隠れ層を繋いでいる重みが学習されたものである
  • 明るい色が正の値,暗い色が負の値である
  • y軸が入力特徴量,x軸が100の隠れユニットである
  • 重みが小さい特徴量はこのモデルにとって重要でない

長所・短所・パラメータ

  • 大量のデータを使って非常に複雑なモデルを構築することができる
  • しかし複雑なモデルは訓練時間が多く必要であることや,慎重なデータの前処理が必要である
  • またパラメータの調整も重要であるため,様々な調整方法を知る必要がある
  • 様々な特徴量をもつデータに関しては,決定木などを用いるべきである

ニューラルネットワークのパラメータについて

  • ニューラルネットワークで最も重要なパラメータは,隠れ層の数と隠れユニットの数である
  • 隠れ層は1つか2つで始め,あとから拡張するのが良い
  • モデルの複雑さの指標として重み(係数)が考えられる
  • 重みの数え方として,100の特徴量をもつ2クラス分類データセットで隠れ層のユニット数が100つの場合の重みは,100(特徴量数) x 100(隠れ層ユニット数)の10,000(入力と隠れ層間の重み) + 100(隠れ層と出力層間の重み) = 10,100(重み)となる
  • さらに100の隠れユニットをもつ第二隠れ層を追加すると100(第一隠れユニット数) x 100(第二隠れユニット数) = 10,000ができるため,このモデルの隠れユニット数は10,100 + 10,000 = 20,100となる
  • ニューラルネットワークのパラメータを調整する方法としてまず過剰適合できるように大きなネットワークを作り,それからネットワークを小さくするか,alphaの調整を行い汎化性能を強化する
  • パラメータを学習するときに用いるアルゴリズムも考える必要があり,簡単に使えるアルゴリズムとしてadam(ほとんどのケースで機能するが,データのスケールには敏感)lbfgs(頑健だが,モデルやデータセットが大きなものだと訓練に時間がかかる)の2つがある
  • lbfgsより高度なものとしてsgdがあるがこちらはパラメータを多く設定する必要があり難易度が高いため,基本的にはadamlbfgsを使えば良い
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google 翻訳 api

GcpでGoogle翻訳apiと画像のocrのapiを組み合わせて
画像から文字を抜き出して翻訳するやつを暇つぶしに作ってみようと思った
一時間程度で簡単に出来ると思ってたが、翻訳部分が本番環境でデプロイするとエラーにずっとなる。
翻訳部分はgooleの公式サイトの見本をコピペしたので、Joinの認証部分がおかしいものだとずっと思ってたが直らずにずっとトライアンドエラーを繰り返してた。
それでもエラーが出るから1行づつ削ってデプロイしてみた。
googleの公式の記述では無理な仕様になってたらしい。。
手任せさせやがってこう言うのが1番イラつく。

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

正規表現によるスクレイピング

正規表現によるスクレイピング

「Python クローリング&スクレイピング」という本を読んでいて詰まってしまい解決した時の方法を載せます。

p49 リスト2.11
このまま打ち込むと
①UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 130: illegal multibyte sequence
①を解決しても
②AttributeError: 'NoneType' object has no attribute 'group'
というエラーが帰ってきてしまう。

  • ①の解決方法  openメソッドにencoding=utf-8を追加
  • ②の解決方法 4つある「title =」 をtry: except: で囲みexcept:の中身はtitle = None とする。

これでURLと書籍のタイトルが取得できるはず。

参考サイト
https://codeday.me/jp/qa/20190511/794837.html

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

データ収集からAI開発、Webアプリ公開まで全てPythonで済ませた話

はじめに

こんにちは、はなたです。
今回は(一般的な?)機械学習プロジェクトの一連の流れである

データ収集→データ整形→機械学習(AI)モデル開発→モデル活用・公開

を全てPythonでやってみたよ、という記事です。プロジェクトと書きましたが、あくまで個人の趣味開発レベルです。

動機

大学院に入学してから機械学習について勉強し始めたのですが、実践する機会があまりありませんでした。kaggleは精度を競うゲームみたいな感じで個人的にはそこまで惹かれておらず、また自分で何か機械学習のモデルを作ろうと思っても「別に予測したいものなんてねえよ(笑)」という感じでした。

つい先日、Webスクレイピングの記事を書いたのですが、これを書き終えた後に今回のテーマを閃きました。今回のテーマは「コード進行から曲のキーを判別するAIを作る」です。自分の趣味である音楽の内容かつ、他に誰もやっていなさそうなアイデアだったので、寝食を忘れて開発に没頭しました。

完成したWebアプリがこちら

Djangoを使用して開発、herokuで公開しています。

https://keyjudgeai.herokuapp.com/kj_ai/

gui_do.gif
動作の様子

コード譜掲載サイトで有名なU-fretに掲載されている曲のURLを入力すると、その曲のキーをAIが判断して表示してくれます。
※今のところU-fret以外のサイトは非対応です。今後、他のサイトやcsvインポートに対応するかも?

開発手順

以下の順序で開発しました。それぞれ詳細は記事にしましたので、興味のある部分を読んで頂ければ幸いです。

0.設計編(キーを判別するAIとは?)
今回の目的である「キーを判別するAI」をプログラムに落とし込む過程が書いてあります。「そもそもコード進行とかキーってなんぞや?」という方のために、その辺の音楽理論についても軽く触れています。

1.データ収集(クローリング)
今回作るAIは機械学習のモデルなので、訓練データが必要です。訓練データ収集について書いてあります。

2.データ整形(スクレイピング)
集めたデータを機械学習モデルに突っ込めるよう、データ整形を行います。コード進行の情報を上手く扱うために、クラス設計を行いました。

3.xgboostを用いたAIの開発←執筆中です
機械学習モデルとハイパーパラメータ選定について書いています。

4.Djangoを用いたWebアプリ開発
Django+Herokuでのデプロイについて書いてあります。

おわりに

データ収集からWebアプリ開発まで、最初から最後までやり遂げられて非常に満足しています。しかし、モデルの予測精度やWebアプリの見た目など改善すべきところはたくさんあるので、時間を見つけて修正していきたいと思います。ここ直した方が良いよ、というコメントをお待ちしております!!!

あと予測が全然合ってねえよ!って曲がありましたら、教えていただけると幸いでございます。

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