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

Nornir 3.0の注意点とNETCONF 経由で、Cisco IOS XE ホスト名の取得

はじめに

この記事はシスコの同志による Advent Calendar の一部として投稿しています
- 2017年版: https://qiita.com/advent-calendar/2017/cisco
- 2018年版: https://qiita.com/advent-calendar/2018/cisco
- 2019年版: https://qiita.com/advent-calendar/2019/cisco
- 2020年版: https://qiita.com/advent-calendar/2020/cisco (今ここ)

Nornir

NornirとはPython製自動化フレームワークのことでAnsibleなどに近いものになっています。
今回のブログでは3.0を動かす注意点とNETCONFでhost名を取得するコードについて書きます

接続方法
- Netmiko
- NAPALM
- NETCONF

Nornir3.0から2.0の時より大きく変わって過去のプログラミングのソースコードが使えなくなる部分が多く
注意点が多くなっています。
特に個別でpluginをインストールする必要が出たりします。pluginを理解しないとサンプルコードを書くことも出来ないです。

Nornirでよく出るエラーの例を出します。

ModuleNotFoundError: No module named 'nornir.plugins.tasks.networking'

2020.09.01バージョンだとエラーは出ないのですが最新の3.0ではエラーが出ます。何も考えないで過去のコードや公式Docのチュートリアルはモジュールないよのエラーで埋め尽くされることでしょう

自分が入れてエラーが出なくなったモジュールをいくつか紹介します。

~ (nornir) KATSHIMA:gg katshima$ pip install nornir
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_napalm
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_netmiko
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_netbox
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_ansible
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_scrapli
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_utils
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_jinja2

上記のModuleNotFoundErrorはnornir_scrapliで解消されたのでnornir_scrapliは必須なのかなと思ってます
以上のモジュールは入れておかないとまともに動かない感じあります。
残りのプラグイン等は以下のページにdescriptionと共に書かれているので参考にすると良いと思います。
https://nornir.tech/nornir/plugins/

ホスト名を取得する

環境

環境
Cisco IOS-XE (16.11)
Cisco DevNet SandBox
Python 3.7.4
nornir 3.0.0a

このブログのすべての例では、Cisco Devnetが提供するCisco SandBox環境を使用します。​Devnetを見に行きましょう。主にIOS XEのSandBoxを使用しています。自動化を試験するときにわざわざ機材を準備しなくていいので本当に楽です。

config.yml
inventory/hosts.yml 
inventory/groups.yml
inventory/defaults.yml

config.yaml

---
runners:
  plugin: threaded
  options:
    num_workers: 10

inventory:
  plugin: "SimpleInventory"
  options: 
    host_file: "./inventory/hosts.yml"
    group_file: "./inventory/groups.yml"
    defaults_file: "./inventory/defaults.yml"

hosts.yaml

---
DEV01: 
  hostname: ios-xe-mgmt.cisco.com
  groups: 
    - Devnet

DEV02: 
  hostname: ios-xe-mgmt-latest.cisco.com
  groups: 
    - Devnet

default.yml

---
platform: ios
port: 8181
username: "developer"
password: "C1sco12345"

groups.yml

---
Devnet: 
  data: 
    ntp: 
      servers: 
        - 1.1.1.1

hostget.py

from nornir import InitNornir
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')
print(nornir.inventory.hosts)

r1 = nornir.inventory.hosts['DEV01']

print(f"Group: {r1.groups}")
print(f"Hostname: {r1.hostname}")
print(f"Username: {r1.username}")
print(f"Password: {r1.password}")

実行結果

(nornir) KATSHIMA-M-J6EA:non katshima$ python aa.py
{'DEV01': Host: DEV01, 'DEV02': Host: DEV02}
Group: [Group: Devnet]
Hostname: ios-xe-mgmt.cisco.com
Username: None
Password: None

 感想

2日間粘ってようやく動きました。
まだまだドキュメントが充実していないのでまだまだこれからの自動化フレームワークな気がしますが
いろんな接続方法を簡単に指定できるのはとても魅力的だと思います。

次回はいろんな機器の状態を持ってくるコードを紹介しようと思います。

コードはGithubにもあげます。よかったらフォローしてください
https://github.com/Katsuya414

参考記事

https://tekunabe.hatenablog.jp/entry/2020/06/14/nornir_netconf_get
https://nornir.readthedocs.io/en/latest/plugins/index.html
https://blog.wimwauters.com/networkprogrammability/2020-09-29_nornir3.0_introduction/
https://github.com/wiwa1978/blog-hugo-netlify-code/tree/master/Nornir_Introduction

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

Nornir 3.0の注意点とCisco IOS XE ホスト名の取得

はじめに

この記事はシスコの同志による Advent Calendar の一部として投稿しています
- 2017年版: https://qiita.com/advent-calendar/2017/cisco
- 2018年版: https://qiita.com/advent-calendar/2018/cisco
- 2019年版: https://qiita.com/advent-calendar/2019/cisco
- 2020年版: https://qiita.com/advent-calendar/2020/cisco (今ここ)

Nornir

NornirとはPython製自動化フレームワークのことでAnsibleなどに近いものになっています。
今回のブログでは3.0を動かす注意点とNETCONFでhost名を取得するコードについて書きます

接続方法
- Netmiko
- NAPALM
- NETCONF

Nornir3.0から2.0の時より大きく変わって過去のプログラミングのソースコードが使えなくなる部分が多く
注意点が多くなっています。
特に個別でpluginをインストールする必要が出たりします。pluginを理解しないとサンプルコードを書くことも出来ないです。

Nornirでよく出るエラーの例を出します。

ModuleNotFoundError: No module named 'nornir.plugins.tasks.networking'

2020.09.01バージョンだとエラーは出ないのですが最新の3.0ではエラーが出ます。何も考えないで過去のコードや公式Docのチュートリアルはモジュールないよのエラーで埋め尽くされることでしょう

自分が入れてエラーが出なくなったモジュールをいくつか紹介します。

~ (nornir) KATSHIMA:gg katshima$ pip install nornir
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_napalm
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_netmiko
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_netbox
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_ansible
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_scrapli
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_utils
~ (nornir) KATSHIMA:gg katshima$ pip install nornir_jinja2

上記のModuleNotFoundErrorはnornir_scrapliで解消されたのでnornir_scrapliは必須なのかなと思ってます
以上のモジュールは入れておかないとまともに動かない感じあります。
残りのプラグイン等は以下のページにdescriptionと共に書かれているので参考にすると良いと思います。
https://nornir.tech/nornir/plugins/

ホスト名を取得する

環境

環境
Cisco IOS-XE (16.11)
Cisco DevNet SandBox
Python 3.7.4
nornir 3.0.0a

このブログのすべての例では、Cisco Devnetが提供するCisco SandBox環境を使用します。​Devnetを見に行きましょう。主にIOS XEのSandBoxを使用しています。自動化を試験するときにわざわざ機材を準備しなくていいので本当に楽です。

config.yml
inventory/hosts.yml 
inventory/groups.yml
inventory/defaults.yml

config.yaml

---
runners:
  plugin: threaded
  options:
    num_workers: 10

inventory:
  plugin: "SimpleInventory"
  options: 
    host_file: "./inventory/hosts.yml"
    group_file: "./inventory/groups.yml"
    defaults_file: "./inventory/defaults.yml"

hosts.yaml

---
DEV01: 
  hostname: ios-xe-mgmt.cisco.com
  groups: 
    - Devnet

DEV02: 
  hostname: ios-xe-mgmt-latest.cisco.com
  groups: 
    - Devnet

default.yml

---
platform: ios
port: 8181
username: "developer"
password: "C1sco12345"

groups.yml

---
Devnet: 
  data: 
    ntp: 
      servers: 
        - 1.1.1.1

hostget.py

from nornir import InitNornir
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')
print(nornir.inventory.hosts)

r1 = nornir.inventory.hosts['DEV01']

print(f"Group: {r1.groups}")
print(f"Hostname: {r1.hostname}")
print(f"Username: {r1.username}")
print(f"Password: {r1.password}")

実行結果

(nornir) KATSHIMA-M-J6EA:non katshima$ python aa.py
{'DEV01': Host: DEV01, 'DEV02': Host: DEV02}
Group: [Group: Devnet]
Hostname: ios-xe-mgmt.cisco.com
Username: None
Password: None

 感想

2日間粘ってようやく動きました。
まだまだドキュメントが充実していないのでまだまだこれからの自動化フレームワークな気がしますが
いろんな接続方法を簡単に指定できるのはとても魅力的だと思います。

次回はいろんな機器の状態を持ってくるコードを紹介しようと思います。

コードはGithubにもあげます。よかったらフォローしてください
https://github.com/Katsuya414

参考記事

https://tekunabe.hatenablog.jp/entry/2020/06/14/nornir_netconf_get
https://nornir.readthedocs.io/en/latest/plugins/index.html
https://blog.wimwauters.com/networkprogrammability/2020-09-29_nornir3.0_introduction/
https://github.com/wiwa1978/blog-hugo-netlify-code/tree/master/Nornir_Introduction

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

和文テキストから主語・動詞(述語)・目的語のSVO(SPO)Triple setを抽出する関数

作ったもの

get_svo_triple.py

( 文を1個、parseする例 )

Python3.9.0
>>> from get_svo_triple import *
>>>
>>> sentence = "ムスカさんは昨日、ジェラート屋さんでアフォガードをたくさんた食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', 'たくさんた->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんは昨日、アフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんは昨日、赤い屋根のジェラート屋さんでアフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'ジェラート屋さんで->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんはおとといと昨日、赤い屋根のジェラート屋さんで、アフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ジェラート屋さんで、->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんはおとといと昨日、赤い屋根のジェラート屋さんでアフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'ジェラート屋さんで->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>> 

( 文を複数件、parseする例 )

(サンプルテキストの出典)

NHK NewsWeb(国際面)2020年11月30日 22時03分付配信記事

Python3.9.0
>>> sentence1 = "イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました。"
>>> sentence2 = "この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました。"
>>> 
>>> 
>>> result_list = [return_svo_triple(sentence) for sentence in [sentence1, sentence2]]
>>> 
>>> from pprint import pprint
>>> 
>>> pprint(result_list)
[({'主語': '武器は', '目的語': 'イスラエル製だと', '述語': '伝えました。'},
  ['事件で、->伝えました。',
   '国営テレビは->伝えました。',
   '話として、->伝えました。',
   '武器は->伝えました。',
   'イスラエル製だと->伝えました。']),
 ({'主語': '英語チャンネルは、', '目的語': '使われていた」と', '述語': '伝えました。'},
  ['事件について、->伝えました。',
   '英語チャンネルは、->伝えました。',
   '30日、->伝えました。',
   '話として->伝えました。',
   '使われていた」と->伝えました。'])]
>>> 

( ニュース記事の全文を処理する場合 )

Python3.9.0
>>> document = """
... イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました。イランの国防軍需相は「暗殺に対しては必ずやり返す」と改めて報復を警告しています。
... 
... イラン政府によりますと、27日、国防軍需省の研究開発部門トップで、核開発技術を研究していたファクリザデ氏が何者かに銃撃されて死亡しました。
... 
... この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました。
... 
... また、国営テレビのアラビア語チャンネルも「衛星を通じて、操作されたイスラエル製の武器が使われた」と伝えています。
... 
... 一方、保守系のファルス通信は、現場に実行犯の姿はなく、遠隔操作で作動する機関銃が使われた可能性があると伝えています。
... 
... この事件について30日、イスラエルの諜報相は地元のラジオ番組で「誰がやったのかは知らない」とコメントしています。
... 
... イランの首都テヘランで30日、ファクリザデ氏の葬儀が営まれ、参列したハタミ国防軍需相は「イラン国民の暗殺に対しては必ずやり返す。犯罪を企て、実行したものたちに罰を与える」と述べ、改めて報復を警告しています。"""
>>> 
>>> document = document.replace(" ", "").replace(" ", "").replace("\n", "")
>>> sentence_list = document.split("。")
>>> pprint(len(sentence_list))
10
>>> sentence_list = [sentence for sentence in sentence_list if not(sentence == "")] 
>>> pprint(sentence_list)
['イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました',
 'イランの国防軍需相は「暗殺に対しては必ずやり返す」と改めて報復を警告しています',
 'イラン政府によりますと、27日、国防軍需省の研究開発部門トップで、核開発技術を研究していたファクリザデ氏が何者かに銃撃されて死亡しました',
 'この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました',
 'また、国営テレビのアラビア語チャンネルも「衛星を通じて、操作されたイスラエル製の武器が使われた」と伝えています',
 '一方、保守系のファルス通信は、現場に実行犯の姿はなく、遠隔操作で作動する機関銃が使われた可能性があると伝えています',
 'この事件について30日、イスラエルの諜報相は地元のラジオ番組で「誰がやったのかは知らない」とコメントしています',
 'イランの首都テヘランで30日、ファクリザデ氏の葬儀が営まれ、参列したハタミ国防軍需相は「イラン国民の暗殺に対しては必ずやり返す',
 '犯罪を企て、実行したものたちに罰を与える」と述べ、改めて報復を警告しています']
>>> 
>>> result_list = [return_svo_triple(sentence) for sentence in sentence_list]
>>> pprint(result_list)
[({'主語': '武器は', '目的語': 'イスラエル製だと', '述語': '伝えました'},
  ['事件で、->伝えました',
   '国営テレビは->伝えました',
   '話として、->伝えました',
   '武器は->伝えました',
   'イスラエル製だと->伝えました']),
 ({'主語': '国防軍需相は', '目的語': '報復を', '述語': '警告しています'},
  ['国防軍需相は->警告しています', 'やり返す」と->警告しています', '改めて->警告しています', '報復を->警告しています']),
 ({'述語': '死亡しました'}, ['よりますと、->死亡しました', '27日、->死亡しました', '銃撃されて->死亡しました']),
 ({'主語': '英語チャンネルは、', '目的語': '使われていた」と', '述語': '伝えました'},
  ['事件について、->伝えました',
   '英語チャンネルは、->伝えました',
   '30日、->伝えました',
   '話として->伝えました',
   '使われていた」と->伝えました']),
 ({'主語': 'アラビア語チャンネルも', '目的語': '使われた」と', '述語': '伝えています'},
  ['また、->伝えています', 'アラビア語チャンネルも->伝えています', '使われた」と->伝えています']),
 ({'目的語': 'あると', '述語': '伝えています'}, ['一方、->伝えています', 'あると->伝えています']),
 ({'主語': '諜報相は', '目的語': '知らない」と', '述語': 'コメントしています'},
  ['事件について->コメントしています',
   '30日、->コメントしています',
   '諜報相は->コメントしています',
   'ラジオ番組で->コメントしています',
   '知らない」と->コメントしています']),
 ({'主語': '暗殺に対しては', '目的語': '暗殺に対しては', '述語': 'やり返す'},
  ['営まれ、->やり返す', 'ハタミ国防軍需相は->やり返す', '暗殺に対しては->やり返す', '必ず->やり返す']),
 ({'目的語': '報復を', '述語': '警告しています'},
  ['述べ、->警告しています', '改めて->警告しています', '報復を->警告しています'])]
>>> 

問題意識

係り受け解析器を用いて、テキストデータから、SVO(SPO)のTriple set([主語, 動詞(述語), 目的語])を返す処理を行う実装コードを探してみたところ、なかなか見当たりませんでした

そのため、備忘録まで、この記事を立ててみました。

時間をかけて方々を探してみたところ、見つけた以下のコードがうまく動きました。

東邦大学 「2014-01-29 CaboChaによる係り受け解析の利用 ~ 文の主節の骨組を取り出す 」

東邦大学の研究室が公開中のコードを、以下の関数(メソッド)に書き換えてみました。

定義した関数(メソッド)のInput->Outputの入出力仕様
・引数と返り値: str -> Tuple[Dict, List]
・引数に渡せるのは、1つの文(sentence)だけです。
・引数に渡すstr型のデータは、分かち書きにする必要はありません。平文のまま渡します。

東邦大学のコードを関数(メソッド)で包んだスクリプト

get_svo_triple.py
import CaboCha
import sys
import codecs
# https://docs.python.org/ja/3.6/library/typing.html
from typing import Dict, Tuple, List

def return_svo_triple(sentence:str) -> Tuple[Dict, List]:
    c = CaboCha.Parser()
    tree = c.parse(sentence)
    size = tree.size()
    myid = 0
    ku_list = []
    ku = ''
    ku_id = 0
    ku_link = 0
    kakari_joshi = 0
    kaku_joshi = 0

    for i in range(0, size):
        token = tree.token(i)
        if token.chunk:
            if (ku!=''):
                ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  #前 の句をリストに追加

            kakari_joshi = 0
            kaku_joshi = 0
            ku = token.normalized_surface
            ku_id = myid
            ku_link = token.chunk.link
            myid=myid+1
        else:
            ku = ku + token.normalized_surface

        m = (token.feature).split(',')
        if (m[1] == u'係助詞'):
            kakari_joshi = 1
        if (m[1] == u'格助詞'):
            kaku_joshi = 1

    ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  # 最後にも前の句をリストに追加
    for k in ku_list:
        if (k[2]==-1):  # link==-1?      # 述語である
            jutsugo_id = ku_id  # この時のidを覚えておく
    #述語句
    predicate_word = [k[0] for k in ku_list if (k[1]==jutsugo_id)]
    #for k in ku_list:
    #   if (k[1]==jutsugo_id):  # jutsugo_idと同じidを持つ句を探す
    #       print(k[1], k[0], k[2], k[3], k[4])
    #述語句に係る句
    # jutsugo_idと同じidをリンク先に持つ句を探す
    word_to_predicate_list = [k[0] for k in ku_list if k[2]==jutsugo_id]
    # 述語句に係る句 -> 述語句
    svo_arrow_text = [str(word_to_predicate) + "->" + str(predicate_word[0]) for word_to_predicate in word_to_predicate_list]
    #print(svo_arrow_text)

    svo_dict = {}
    for num, k in enumerate(ku_list):
        if (k[2]==jutsugo_id):  # jutsugo_idと同じidをリンク先に持つ句を探す
            if (k[3] == 1):
                subject_word = k[0]
                svo_dict["主語"] = subject_word
                #print(subject_word)
            if (k[4] == 1):
                object_word = k[0]
                svo_dict["目的語"] = object_word
                #print(object_word)
        if (k[1] == jutsugo_id):
                predicate_word = k[0]
                svo_dict["述語"] = predicate_word
                #print(predicate_word)

    return (svo_dict, svo_arrow_text)

今後やりたいこと

<1つ目>

・ 以下の論文ほかを参考に、Web空間上のテキストデータから、出来事間の時系列因果構造を抽出したい。
・ さらに、獲得した出来事間の時系列因果構造を、視覚的に把握しやすいように、グラフ構造のネットワーク図に描画したい。

石井ほか 「SVO構造を用いた因果関係ネットワーク構築手法について」

<2つ目>

[主語 -> 述語(動詞など) -> 目的語・補語]の三つ組データ(Triple setを、Prologなどの論理推論ができるプログラミング言語に与えることで、複数の三つ組データ(Triple set)間の因果関係や概念意味関係を用いた知識推論を行ってみたい。

<3つ目>

MaskedTransformerVideoBERTなどの学習済みモデルを用いて、静止画像や動画の内容説明文(内容要約文、キャプション文)を生成し、その説明文に含まれる[主語 -> 述語(動詞など) -> 目的語・補語]の三つ組データ(Triple set)を抽出したい。
・ 動画の説明文から抜き出した「主語」や「目的語」や「補語」の単語を、DB(データベース)に格納して、固有表現抽出器(Named Entitity Recognitonを用いて、「人名」や「組織名」や「地名」といった固有表現ラベルを紐付けたい。
・ その結果、特定の人物や組織や地名が登場する静止画や動画を、容易に検索できるプラットフォームを構築したい。

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

和文から主語・動詞(述語)・目的語のSVO(SPO)Triple setを抽出する関数

作ったもの

get_svo_triple.py

( 文を1個、parseする例 )

Python3.9.0
>>> from get_svo_triple import *
>>>
>>> sentence = "ムスカさんは昨日、ジェラート屋さんでアフォガードをたくさんた食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', 'たくさんた->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんは昨日、アフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんは昨日、赤い屋根のジェラート屋さんでアフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'ジェラート屋さんで->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんはおとといと昨日、赤い屋根のジェラート屋さんで、アフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ジェラート屋さんで、->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>>
Python3.9.0
>>> sentence = "ムスカさんはおとといと昨日、赤い屋根のジェラート屋さんでアフォガードをたくさん食べた。"
>>> (result1, result2) = return_svo_triple(sentence)
>>> 
>>> print(result1)
{'主語': 'ムスカさんは', '目的語': 'アフォガードを', '述語': '食べた。'}
>>> 
>>> print(result2)
['ムスカさんは->食べた。', '昨日、->食べた。', 'ジェラート屋さんで->食べた。', 'アフォガードを->食べた。', 'たくさん->食べた。']
>>> 

( 文を複数件、parseする例 )

(サンプルテキストの出典)

NHK NewsWeb(国際面)2020年11月30日 22時03分付配信記事

Python3.9.0
>>> sentence1 = "イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました。"
>>> sentence2 = "この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました。"
>>> 
>>> 
>>> result_list = [return_svo_triple(sentence) for sentence in [sentence1, sentence2]]
>>> 
>>> from pprint import pprint
>>> 
>>> pprint(result_list)
[({'主語': '武器は', '目的語': 'イスラエル製だと', '述語': '伝えました。'},
  ['事件で、->伝えました。',
   '国営テレビは->伝えました。',
   '話として、->伝えました。',
   '武器は->伝えました。',
   'イスラエル製だと->伝えました。']),
 ({'主語': '英語チャンネルは、', '目的語': '使われていた」と', '述語': '伝えました。'},
  ['事件について、->伝えました。',
   '英語チャンネルは、->伝えました。',
   '30日、->伝えました。',
   '話として->伝えました。',
   '使われていた」と->伝えました。'])]
>>> 

( ニュース記事の全文を処理する場合 )

Python3.9.0
>>> document = """
... イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました。イランの国防軍需相は「暗殺に対しては必ずやり返す」と改めて報復を警告しています。
... 
... イラン政府によりますと、27日、国防軍需省の研究開発部門トップで、核開発技術を研究していたファクリザデ氏が何者かに銃撃されて死亡しました。
... 
... この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました。
... 
... また、国営テレビのアラビア語チャンネルも「衛星を通じて、操作されたイスラエル製の武器が使われた」と伝えています。
... 
... 一方、保守系のファルス通信は、現場に実行犯の姿はなく、遠隔操作で作動する機関銃が使われた可能性があると伝えています。
... 
... この事件について30日、イスラエルの諜報相は地元のラジオ番組で「誰がやったのかは知らない」とコメントしています。
... 
... イランの首都テヘランで30日、ファクリザデ氏の葬儀が営まれ、参列したハタミ国防軍需相は「イラン国民の暗殺に対しては必ずやり返す。犯罪を企て、実行したものたちに罰を与える」と述べ、改めて報復を警告しています。"""
>>> 
>>> document = document.replace(" ", "").replace(" ", "").replace("\n", "")
>>> sentence_list = document.split("。")
>>> pprint(len(sentence_list))
10
>>> sentence_list = [sentence for sentence in sentence_list if not(sentence == "")] 
>>> pprint(sentence_list)
['イランで核科学者が何者かに銃撃されて死亡した事件で、地元の国営テレビは関係者の話として、殺害に使用された武器はイスラエル製だと伝えました',
 'イランの国防軍需相は「暗殺に対しては必ずやり返す」と改めて報復を警告しています',
 'イラン政府によりますと、27日、国防軍需省の研究開発部門トップで、核開発技術を研究していたファクリザデ氏が何者かに銃撃されて死亡しました',
 'この事件について、国営テレビの英語チャンネルは、30日、関係者の話として「現場で回収された武器にはイスラエルの軍事産業につながる特徴があり、イスラエル製の武器が使われていた」と伝えました',
 'また、国営テレビのアラビア語チャンネルも「衛星を通じて、操作されたイスラエル製の武器が使われた」と伝えています',
 '一方、保守系のファルス通信は、現場に実行犯の姿はなく、遠隔操作で作動する機関銃が使われた可能性があると伝えています',
 'この事件について30日、イスラエルの諜報相は地元のラジオ番組で「誰がやったのかは知らない」とコメントしています',
 'イランの首都テヘランで30日、ファクリザデ氏の葬儀が営まれ、参列したハタミ国防軍需相は「イラン国民の暗殺に対しては必ずやり返す',
 '犯罪を企て、実行したものたちに罰を与える」と述べ、改めて報復を警告しています']
>>> 
>>> result_list = [return_svo_triple(sentence) for sentence in sentence_list]
>>> pprint(result_list)
[({'主語': '武器は', '目的語': 'イスラエル製だと', '述語': '伝えました'},
  ['事件で、->伝えました',
   '国営テレビは->伝えました',
   '話として、->伝えました',
   '武器は->伝えました',
   'イスラエル製だと->伝えました']),
 ({'主語': '国防軍需相は', '目的語': '報復を', '述語': '警告しています'},
  ['国防軍需相は->警告しています', 'やり返す」と->警告しています', '改めて->警告しています', '報復を->警告しています']),
 ({'述語': '死亡しました'}, ['よりますと、->死亡しました', '27日、->死亡しました', '銃撃されて->死亡しました']),
 ({'主語': '英語チャンネルは、', '目的語': '使われていた」と', '述語': '伝えました'},
  ['事件について、->伝えました',
   '英語チャンネルは、->伝えました',
   '30日、->伝えました',
   '話として->伝えました',
   '使われていた」と->伝えました']),
 ({'主語': 'アラビア語チャンネルも', '目的語': '使われた」と', '述語': '伝えています'},
  ['また、->伝えています', 'アラビア語チャンネルも->伝えています', '使われた」と->伝えています']),
 ({'目的語': 'あると', '述語': '伝えています'}, ['一方、->伝えています', 'あると->伝えています']),
 ({'主語': '諜報相は', '目的語': '知らない」と', '述語': 'コメントしています'},
  ['事件について->コメントしています',
   '30日、->コメントしています',
   '諜報相は->コメントしています',
   'ラジオ番組で->コメントしています',
   '知らない」と->コメントしています']),
 ({'主語': '暗殺に対しては', '目的語': '暗殺に対しては', '述語': 'やり返す'},
  ['営まれ、->やり返す', 'ハタミ国防軍需相は->やり返す', '暗殺に対しては->やり返す', '必ず->やり返す']),
 ({'目的語': '報復を', '述語': '警告しています'},
  ['述べ、->警告しています', '改めて->警告しています', '報復を->警告しています'])]
>>> 

問題意識

係り受け解析器を用いて、テキストデータから、SVO(SPO)のTriple set([主語, 動詞(述語), 目的語])を返す処理を行う実装コードを探してみたところ、なかなか見当たりませんでした

そのため、備忘録まで、この記事を立ててみました。

時間をかけて方々を探してみたところ、見つけた以下のコードがうまく動きました。

東邦大学 「2014-01-29 CaboChaによる係り受け解析の利用 ~ 文の主節の骨組を取り出す 」

東邦大学の研究室が公開中のコードを、以下の関数(メソッド)に書き換えてみました。

定義した関数(メソッド)のInput->Outputの入出力仕様
・引数と返り値: str -> Tuple[Dict, List]
・引数に渡せるのは、1つの文(sentence)だけです。
・引数に渡すstr型のデータは、分かち書きにする必要はありません。平文のまま渡します。

東邦大学のコードを関数(メソッド)で包んだスクリプト

get_svo_triple.py
import CaboCha
import sys
import codecs
# https://docs.python.org/ja/3.6/library/typing.html
from typing import Dict, Tuple, List

def return_svo_triple(sentence:str) -> Tuple[Dict, List]:
    c = CaboCha.Parser()
    tree = c.parse(sentence)
    size = tree.size()
    myid = 0
    ku_list = []
    ku = ''
    ku_id = 0
    ku_link = 0
    kakari_joshi = 0
    kaku_joshi = 0

    for i in range(0, size):
        token = tree.token(i)
        if token.chunk:
            if (ku!=''):
                ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  #前 の句をリストに追加

            kakari_joshi = 0
            kaku_joshi = 0
            ku = token.normalized_surface
            ku_id = myid
            ku_link = token.chunk.link
            myid=myid+1
        else:
            ku = ku + token.normalized_surface

        m = (token.feature).split(',')
        if (m[1] == u'係助詞'):
            kakari_joshi = 1
        if (m[1] == u'格助詞'):
            kaku_joshi = 1

    ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  # 最後にも前の句をリストに追加
    for k in ku_list:
        if (k[2]==-1):  # link==-1?      # 述語である
            jutsugo_id = ku_id  # この時のidを覚えておく
    #述語句
    predicate_word = [k[0] for k in ku_list if (k[1]==jutsugo_id)]
    #for k in ku_list:
    #   if (k[1]==jutsugo_id):  # jutsugo_idと同じidを持つ句を探す
    #       print(k[1], k[0], k[2], k[3], k[4])
    #述語句に係る句
    # jutsugo_idと同じidをリンク先に持つ句を探す
    word_to_predicate_list = [k[0] for k in ku_list if k[2]==jutsugo_id]
    # 述語句に係る句 -> 述語句
    svo_arrow_text = [str(word_to_predicate) + "->" + str(predicate_word[0]) for word_to_predicate in word_to_predicate_list]
    #print(svo_arrow_text)

    svo_dict = {}
    for num, k in enumerate(ku_list):
        if (k[2]==jutsugo_id):  # jutsugo_idと同じidをリンク先に持つ句を探す
            if (k[3] == 1):
                subject_word = k[0]
                svo_dict["主語"] = subject_word
                #print(subject_word)
            if (k[4] == 1):
                object_word = k[0]
                svo_dict["目的語"] = object_word
                #print(object_word)
        if (k[1] == jutsugo_id):
                predicate_word = k[0]
                svo_dict["述語"] = predicate_word
                #print(predicate_word)

    return (svo_dict, svo_arrow_text)

今後やりたいこと

<1つ目>

・ 以下の論文ほかを参考に、Web空間上のテキストデータから、出来事間の時系列因果構造を抽出したい。
・ さらに、獲得した出来事間の時系列因果構造を、視覚的に把握しやすいように、グラフ構造のネットワーク図に描画したい。

石井ほか 「SVO構造を用いた因果関係ネットワーク構築手法について」

<2つ目>

[主語 -> 述語(動詞など) -> 目的語・補語]の三つ組データ(Triple setを、Prologなどの論理推論ができるプログラミング言語に与えることで、複数の三つ組データ(Triple set)間の因果関係や概念意味関係を用いた知識推論を行ってみたい。

<3つ目>

MaskedTransformerVideoBERTなどの学習済みモデルを用いて、静止画像や動画の内容説明文(内容要約文、キャプション文)を生成し、その説明文に含まれる[主語 -> 述語(動詞など) -> 目的語・補語]の三つ組データ(Triple set)を抽出したい。
・ 動画の説明文から抜き出した「主語」や「目的語」や「補語」の単語を、DB(データベース)に格納して、固有表現抽出器(Named Entitity Recognitonを用いて、「人名」や「組織名」や「地名」といった固有表現ラベルを紐付けたい。
・ その結果、特定の人物や組織や地名が登場する静止画や動画を、容易に検索できるプラットフォームを構築したい。

MaskedTransformerの「再現実装コード」は、Albert社さんのリポジトリから取得可能です。

( GitHub )ALBERT-Inc/blog_masked_transformer

公開されている学習済み重みファイルはこちらからダウンロードできます。これを解凍し、yc2-2L-e2e-maskというディレクトリの下にある重みファイルを利用して学習してください。

Albert社ブログ 「動画認識手法の紹介とキャプション生成手法Masked Transformerについての解説」

Show and Tell論文学習済みモデルの利用方法は以下が参考になる。
@48saaanさんのQiita記事 「「Show and Tell」の TensorFlow をCPUでお試し」

トレーニング済みモデルの準備
GitHubのtensorflow/modelsのIssues「Pretrained model for img2txt? #466」で「here are links to a pre-trained model:」を検索すると、その書き込みの下に3つのリンクがあります。
そこから「im2txt_2016_10_11.2000000.tar.gz」(←finetuned)と「word_counts.txt」を~/test/models/research/im2txtにコピー

解凍もしておきます

tar xf im2txt_2016_10_11.2000000.tar.gz
rm im2txt_2016_10_11.2000000.tar.gz
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】virtualenvで仮想環境を作成する(コマンドプロンプト)

自分用メモ。Windows。
コードの頭に">>"とついているのは、コマンドプロンプトで打ち込むコマンド。

前提

Pythonがすでにインストールされてある。

環境を作成したいディレクトリを開く

>> cd C:\Users\UserName\Documents\MyApp

または、エクスプローラーでディレクトリを開いて、パスに「cmd」と打ち込むと、そのディレクトリでコマンドプロンプトが開ける
flasknote 2020_11_30 22_41_02.jpg

virtualenvで仮想環境を作成

virtualenvをまだpipでインストールしていない人は、次のコマンドでインストールする。

>> py -m pip install virtualenv

次のコマンドで仮想環境を作成する

>> py -m virtualenv .venv

(自分のPCでは)pipやvirtualenvの環境変数は設定されていないので、 それらのコマンドを走らせることはできない。
"py -m" をつけると、pythonがすでにpipでインストールされたライブラリからコマンドをとってきてくれる。
正直この辺の原理はよく知らない。

これで、".venv"というフォルダが作成される。中には色々ファイルがある。
今後、pipでインストールするライブラリは、この.venvの中に入っていく。

.venv の部分は、"hoge"でも何でもいい。
でも通例として venvか .venv と名付けることが多い(ように思われる)。
.venvとつけておけば、そのフォルダが仮想環境のフォルダとすぐに分かるから、そうしたほうがいい。

仮想環境に切り替える

このままだと、仮想環境を作成しただけであって、"いま、この仮想環境を使用していますよ~"ということにはなっていない。
作成した仮想環境を使用中にするには、次のコマンドをうつ。

>> .venv\Scripts\Activate

これは、さっき作成された.venv/Scriptsフォルダ内のActivateを実行している。
こうすると、コマンドプロンプトの頭に(.venv)とつくようになる。
image.png

仮想環境内でライブラリをインストールする

コマンドプロンプトの頭に(.venv)がついている状態で

>> pip install hogehoge

のようにpipでライブラリをインストールすると、仮想環境内にインストールされる。
仮想環境の使用中は、py -m はいらない。

仮想環境を終了する

仮想環境の使用を終了するには、次のコマンドをうつ。

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

Cisco PSIRT OpenVuln API を使ってみる

はじめに

シスコは サポート 契約のあるパートナーや、ユーザーが利用可能な Support APIを公開しています。
Cisco Support APIs
Cisco Support APIは、受発注に関わるものや、サポートに関わるものなど多く存在しますが、今回は、シスコアカウントがあれば、手軽に使える Cisco PSIRT OpenVuln APIを使ってみます。

Cisco PSIRT OpenVuln とは

OpenVuln API は簡単に言うと、Cisco PSIRT(Product Security Incident Response Team) の情報、脆弱性情報を REST API を使って、様々なフォーマットで、効率的に取得できる API です。

Cisco PSIRT OpenVuln

これを使うと、REST APIで、特定のIOS version の脆弱性情報や、セキュリティアドバイザリの詳細などを取得することができます。

アカウントセットアップ

まず、Cisco Support API を利用する場合は、 https://apiconsole.cisco.com へアクセスし、APIを有効にする必要があります。

手順

  1. https://apiconsole.cisco.com へアクセスし Cisco.com アカウントでログイン
  2. My Apps & Keys タブをクリック
  3. Register an Application で、アプリケーション名など必要事項を記入(赤枠の部分を埋めます)し、登録

そうすることで、API 利用に必要な KEY と SECRET が発行されます
apiconsole.png

Python モジュール openVulnQuery を使う

アカウントの設定が完了すれば、 REST API で情報取得ができます。
今回は、コマンドラインツールとしても使える、openVulnQuery が便利なので、これを使ってみます。

GitHub - CiscoPSIRT/openVulnQuery: A Python-based client for the Cisco openVuln API

インストール & セットアップ

Pip でインストールをするだけです。

pip install openVulnQuery

私の環境では、下記の通り PATH を通ってないよとエラーが出たのでコマンドラインツールとして利用するために、 .bash_profile に PATHを追加

WARNING: The script chardetect is installed in '/Users/USER_ID/Library/Python/3.8/bin' which is not on PATH.

.bash_profile
  :
export PATH="/Users/USER_ID/Library/Python/3.8/bin:$PATH"
  :

openVulnQuery を使うためには、環境変数で、KEY と Secret を渡すか、もしくは、json 形式のファイルに KEY と Secret を記載して、オプションで渡すことで APIにアクセスをします。

  • 環境変数で渡す場合
$export CLIENT_ID="xxxxxxx"
$export CLIENT_SECRET="yyyyyyy"
  • JSON形式のファイルで渡す場合 下記の形式の credential 用のファイルを、一つ作成します。
credential.json
{
      "CLIENT_ID": "xxxxxxx",
      "CLIENT_SECRET": "yyyyyyyy"
}

openVulnQuery の使い方

openVlunQuery に、credential file を --config で指定して、実行すれば、データが取得できます。

$ openVulnQuery -h
usage: openVulnQuery [-h]
                     (--all | --advisory <advisory-id> | --cve <CVE-id> | --latest number | --severity [critical, high, medium, low] | --year year | --product product_name | --ios_xe iosxe_version | --ios ios_version | --nxos nxos_version | --aci aci_version)
                     [--csv filepath | --json filepath] [--first_published YYYY-MM-DD:YYYY-MM-DD | --last_published YYYY-MM-DD:YYYY-MM-DD] [-c] [-f  [...]] [--user-agent string]
                     [--config filepath]

詳細
Cisco OpenVuln API Command Line Interface

optional arguments:
  -h, --help            show this help message and exit
  --all                 Retrieves all advisiories
  --advisory <advisory-id>
                        Retrieve advisories by advisory id
  --cve <CVE-id>        Retrieve advisories by cve id
  --latest number       Retrieves latest (number) advisories
  --severity [critical, high, medium, low]
                        Retrieve advisories by severity (low, medium, high, critical)
  --year year           Retrieve advisories by year
  --product product_name
                        Retrieve advisories by product names
  --ios_xe iosxe_version
                        Retrieve advisories affecting user inputted ios_xe version.Only one version at a time is allowed.
  --ios ios_version     Retrieve advisories affecting user inputted ios version.Only one version at a time is allowed.
  --nxos nxos_version   Retrieve advisories affecting user inputted NX-OS (in standalone mode) version.Only one version at a time is allowed.
  --aci aci_version     Retrieve advisories affecting user inputted NX-OS (in ACI mode) version.Only one version at a time is allowed.
  --csv filepath        Output to CSV with file path
  --json filepath       Output to JSON with file path
  --first_published YYYY-MM-DD:YYYY-MM-DD
                        Filter advisories based on first_published date YYYY-MM-DD:YYYY-MM-DD USAGE: followed by severity or all
  --last_published YYYY-MM-DD:YYYY-MM-DD, --last_updated YYYY-MM-DD:YYYY-MM-DD
                        Filter advisories based on last_published date YYYY-MM-DD:YYYY-MM-DD USAGE: followed by severity or all
  -c, --count           Count of any field or fields
  -f  [ ...], --fields  [ ...]
                        Separate fields by spaces to return advisory information. Allowed values are: advisory_id, advisory_title, bug_ids, cves, cvrfUrl, cvss_base_score, cwe,
                        first_fixed, first_published, ios_release, ips_signatures, last_updated, product_names, publication_url, sir, summary
  --user-agent string   Announced User-Agent hedar value (towards service)
  --config filepath     Path to JSON file with config (otherwise fallback to environment variables CLIENT_ID and CLIENT_SECRET, or config.py variables, or fail)

例1) IOS-XE version 16.2.2 に該当するセキュリティアドバイザリIDとタイトルを一覧表示

openVulnQuery --ios_xe 16.2.2 -f advisory_id advisory_title

出力結果
[
    {
        "advisory_id": "cisco-sa-info-disclosure-V4BmJBNF",
        "advisory_title": "Cisco IOS and IOS XE Software Information Disclosure Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-iosxe-isdn-q931-dos-67eUZBTf",
        "advisory_title": "Cisco IOS and IOS XE Software ISDN Q.931 Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-rommon-secboot-7JgVLVYC",
        "advisory_title": "Cisco IOS XE ROM Monitor Software Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-ios-lua-rce-7VeJX4f",
        "advisory_title": "Cisco IOS XE Software Arbitrary Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-esp20-arp-dos-GvHVggqJ",
        "advisory_title": "Cisco IOS XE Software for Cisco ASR 1000 Series 20-Gbps Embedded Services Processor IP ARP Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-iosxe-rsp3-rce-jVHg8Z7c",
        "advisory_title": "Cisco IOS XE Software for Cisco ASR 900 Series Route Switch Processor 3 Arbitrary Code Execution Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-ios-webui-priv-esc-K8zvEWM",
        "advisory_title": "Cisco IOS XE Software Privilege Escalation Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-ios-xe-webui-multi-vfTkk7yr",
        "advisory_title": "Cisco IOS XE Software Web Management  Framework Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-webui-auth-bypass-6j2BYUc7",
        "advisory_title": "Cisco IOS XE Software Web UI Authorization Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-zbfw-94ckG4G",
        "advisory_title": "Cisco IOS XE Software Zone-Based Firewall Denial of Service Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-telnetd-EFJrEzPx",
        "advisory_title": "Telnet Vulnerability Affecting Cisco Products: June 2020"
    },
    {
        "advisory_id": "cisco-sa-tcl-dos-MAZQUnMF",
        "advisory_title": "Cisco IOS and IOS XE Software Tcl Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-ikev2-9p23Jj2a",
        "advisory_title": "Cisco IOS and IOS XE Software Internet Key Exchange Version 2 Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-ssh-dos-Un22sd2A",
        "advisory_title": "Cisco IOS and IOS XE Software Secure Shell Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-sip-Cv28sQw2",
        "advisory_title": "Cisco IOS and IOS XE Software Session Initiation Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-tcl-ace-C9KuVKmm",
        "advisory_title": "Cisco IOS and IOS XE Software Tcl Arbitrary Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-iosxe-ewlc-dos-TkuPVmZN",
        "advisory_title": "Cisco IOS XE Software Catalyst 9800 Series Wireless Controllers Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-iosxe-digsig-bypass-FYQ3bmVq",
        "advisory_title": "Cisco IOS XE Software Digital Signature Verification Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-priv-esc1-OKMKFRhV",
        "advisory_title": "Cisco IOS XE Software Privilege Escalation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-priv-esc3-GMgnGCHx",
        "advisory_title": "Cisco IOS XE Software Privilege Escalation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-web-cmdinj2-fOnjk2LD",
        "advisory_title": "Cisco IOS XE Software Web UI Command Injection Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-webui-PZgQxjfG",
        "advisory_title": "Cisco IOS XE Software Web UI Privilege Escalation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-sxp-68TEVzR",
        "advisory_title": "Cisco IOS, IOS XE, and NX-OS Software Security Group Tag Exchange Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-ios-nxos-onepk-rce-6Hhyt4dC",
        "advisory_title": "Cisco IOS, IOS XE, IOS XR, and NX-OS Software One Platform Kit Remote Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190828-iosxe-rest-auth-bypass",
        "advisory_title": "Cisco REST API Container for IOS XE Software Authentication Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-sip-dos",
        "advisory_title": "Cisco IOS and IOS XE Software Session Initiation Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-tsec",
        "advisory_title": "Cisco IOS and IOS XE Software Change of Authorization Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-http-client",
        "advisory_title": "Cisco IOS and IOS XE Software HTTP Client Information Disclosure Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-identd-dos",
        "advisory_title": "Cisco IOS and IOS XE Software IP Ident Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-sbxss",
        "advisory_title": "Cisco IOS and IOS XE Software Stored Banner Cross-Site Scripting Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-iosxe-codeexec",
        "advisory_title": "Cisco IOS XE Software Arbitrary Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-awr",
        "advisory_title": "Cisco IOS XE Software ASIC Register Write Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-iosxe-fsdos",
        "advisory_title": "Cisco IOS XE Software Filesystem Exhaustion Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-sip-alg",
        "advisory_title": "Cisco IOS XE Software NAT Session Initiation Protocol Application Layer Gateway Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-utd",
        "advisory_title": "Cisco IOS XE Software Unified Threat Defense Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190925-vman",
        "advisory_title": "Cisco NX-OS and IOS XE Software Virtual Service Image Signature Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20160525-ipv6",
        "advisory_title": "Cisco Products IPv6 Neighbor Discovery Crafted Packet Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190612-iosxe-csrf",
        "advisory_title": "Cisco IOS XE Software Web UI Cross-Site Request Forgery Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190513-webui",
        "advisory_title": "Cisco IOS XE Software Web UI Command Injection Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170629-snmp",
        "advisory_title": "SNMP Remote Code Execution Vulnerabilities in Cisco IOS and IOS XE Software"
    },
    {
        "advisory_id": "cisco-sa-20190327-ios-infoleak",
        "advisory_title": "Cisco IOS and IOS XE Software Hot Standby Router Protocol Information Leak Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-ipsla-dos",
        "advisory_title": "Cisco IOS and IOS XE Software IP Service Level Agreement Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-isdn",
        "advisory_title": "Cisco IOS and IOS XE Software ISDN Interface Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-pnp-cert",
        "advisory_title": "Cisco IOS and IOS XE Software Network Plug-and-Play Agent Certificate Validation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-nbar",
        "advisory_title": "Cisco IOS and IOS XE Software Network-Based Application Recognition Denial of Service Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-20190327-call-home-cert",
        "advisory_title": "Cisco IOS and IOS XE Software Smart Call Home Certificate Validation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-afu",
        "advisory_title": "Cisco IOS XE Software Arbitrary File Upload Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-xecmd",
        "advisory_title": "Cisco IOS XE Software Command Injection Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-iosxe-cmdinj",
        "advisory_title": "Cisco IOS XE Software Command Injection Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190327-mgmtacl",
        "advisory_title": "Cisco IOS XE Software Gigabit Ethernet Management Interface Access Control List Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20190109-tcp",
        "advisory_title": "Cisco IOS and IOS XE Software TCP Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-ipsec",
        "advisory_title": "Cisco IOS XE Software and Cisco ASA 5500-X Series Adaptive Security Appliance IPsec Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-cdp-dos",
        "advisory_title": "Cisco IOS and IOS XE Software Cisco Discovery Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-ipv6hbh",
        "advisory_title": "Cisco IOS and IOS XE Software IPv6 Hop-by-Hop Options Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-ospfv3-dos",
        "advisory_title": "Cisco IOS and IOS XE Software OSPFv3 Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-pnp-memleak",
        "advisory_title": "Cisco IOS and IOS XE Software Plug and Play Agent Memory Leak Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-sm1t3e3",
        "advisory_title": "Cisco IOS and IOS XE Software SM-1T3/E3 Service Module Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-vtp",
        "advisory_title": "Cisco IOS and IOS XE Software VLAN Trunking Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-iosxe-cmdinj",
        "advisory_title": "Cisco IOS XE Software Command Injection Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-20180926-digsig",
        "advisory_title": "Cisco IOS XE Software Digital Signature Verification Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-webdos",
        "advisory_title": "Cisco IOS XE Software HTTP Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-macsec",
        "advisory_title": "Cisco IOS XE Software MACsec MKA Using EAP-TLS Authentication Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-sip-alg",
        "advisory_title": "Cisco IOS XE Software NAT Session Initiation Protocol Application Layer Gateway Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-shell-access",
        "advisory_title": "Cisco IOS XE Software Shell Access Authentication Bypass Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180926-webuidos",
        "advisory_title": "Cisco IOS XE Software Web UI Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-smi",
        "advisory_title": "Cisco IOS and IOS XE Software Smart Install Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-smi2",
        "advisory_title": "Cisco IOS and IOS XE Software Smart Install Remote Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-lldp",
        "advisory_title": "Cisco IOS, IOS XE, and IOS XR Software Link Layer Discovery Protocol Buffer Overflow Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-20180328-qos",
        "advisory_title": "Cisco IOS and IOS XE Software Quality of Service Remote Code Execution Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-dhcpr1",
        "advisory_title": "Cisco IOS and IOS XE Software DHCP Version 4 Relay Heap Overflow Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-dhcpr2",
        "advisory_title": "Cisco IOS and IOS XE Software DHCP Version 4 Relay Reply Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-ike",
        "advisory_title": "Cisco IOS and IOS XE Software Internet Key Exchange Memory Leak Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-ipv4",
        "advisory_title": "Cisco IOS XE Software for Cisco Catalyst Switches IPv4 Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-igmp",
        "advisory_title": "Cisco IOS XE Software Internet Group Management Protocol Memory Leak Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-snmp-dos",
        "advisory_title": "Cisco IOS XE Software Simple Network Management Protocol Double-Free Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20180328-privesc1",
        "advisory_title": "Cisco IOS XE Software User EXEC Mode Root Shell Access Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-20180328-xepriv",
        "advisory_title": "Cisco IOS XE Software Web UI Remote Access Privilege Escalation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170419-energywise",
        "advisory_title": "Cisco IOS and IOS XE Software EnergyWise Denial of Service Vulnerabilities"
    },
    {
        "advisory_id": "cisco-sa-20170726-anicrl",
        "advisory_title": "Cisco IOS XE Software Autonomic Networking Infrastructure Certificate Revocation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20171103-bgp",
        "advisory_title": "Cisco IOS XE Software Ethernet Virtual Private Network Border Gateway Protocol Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170927-ike",
        "advisory_title": "Cisco IOS and IOS XE Software Internet Key Exchange Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170927-pnp",
        "advisory_title": "Cisco IOS and IOS XE Software Plug-and-Play PKI API Certificate Validation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170727-ospf",
        "advisory_title": "Multiple Cisco Products OSPF LSA Manipulation Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170726-aniacp",
        "advisory_title": "Cisco IOS and IOS XE Software Autonomic Control Plane Channel Information Disclosure Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170726-anidos",
        "advisory_title": "Cisco IOS and IOS XE Software Autonomic Networking Infrastructure Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170320-ani",
        "advisory_title": "Cisco IOS and IOS XE Software Autonomic Networking Infrastructure Registrar Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20170320-aniipv6",
        "advisory_title": "Cisco IOS and IOS XE Software IPv6 Denial of Service Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20161115-iosxe",
        "advisory_title": "Cisco IOS XE Software Directory Traversal Vulnerability"
    },
    {
        "advisory_id": "cisco-sa-20160916-ikev1",
        "advisory_title": "IKEv1 Information Disclosure Vulnerability in Multiple Cisco Products"
    }
]

例2) CVE ID "CVE-2010-3043" で セキュリティアドバイザリの内容を確認

openVulnQuery --cve CVE-2010-3043

出力結果
[
    {
        "advisory_id": "cisco-sa-20110201-webex",
        "advisory_title": "Multiple Cisco WebEx Player Vulnerabilities",
        "bug_ids": [
            "NA"
        ],
        "cves": [
            "CVE-2010-3041",
            "CVE-2010-3042",
            "CVE-2010-3043",
            "CVE-2010-3044",
            "CVE-2010-3269"
        ],
        "cvrfUrl": "https://tools.cisco.com/security/center/contentxml/CiscoSecurityAdvisory/cisco-sa-20110201-webex/cvrf/cisco-sa-20110201-webex_cvrf.xml",
        "cvss_base_score": "9.3",
        "cwe": [
            "CWE-94"
        ],
        "first_published": "2011-02-01T16:00:00",
        "ips_signatures": [
            {
                "legacy_ips_id": "cisco-sa-20110201-webex",
                "legacy_ips_url": "https://tools.cisco.com/security/center/viewIpsSignature.x?signatureId=36910&signatureSubId=0&softwareVersion=6.0&releaseVersion=S682",
                "release_version": "S682",
                "software_version": "6.0"
            }
        ],
        "last_updated": "2011-02-01T16:00:00",
        "product_names": [
            "NA"
        ],
        "publication_url": "http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20110201-webex",
        "sir": "Critical",
        "summary": "<p>Multiple buffer overflow vulnerabilities exist in the Cisco WebEx\n\t Recording Format (WRF) and Advanced Recording Format (ARF) Players. In some\n\t cases, exploitation of the vulnerabilities could allow a remote attacker to\n\t execute arbitrary code on the system of a targeted user.</p> \n  <p> </p> \n  <p> The Cisco WebEx Players are applications that are used to\n\t play back WebEx meeting recordings that have been recorded on the computer of\n\t an on-line meeting attendee. The players can be automatically installed when\n\t the user accesses a recording file that is hosted on a WebEx server. The player\n\t can also be manually installed for offline playback after downloading the\n\t application from <a target=\"_blank\" href=\"http://www.webex.com\">www.webex.com</a>\n<img alt=\"leavingcisco.com\" height=\"18\" width=\"18\" src=\"http://www.cisco.com/images/exit.gif\" />.</p> \n  <p> </p> \n  <p> If the WebEx recording player was automatically installed,\n\t it will be automatically upgraded to the latest, non-vulnerable version when\n\t users access a recording file that is hosted on a WebEx server. If the WebEx\n\t recording player was manually installed, users will need to manually install a\n\t new version of the player after downloading the latest version from\n\t <a target=\"_blank\" href=\"http://www.webex.com\">www.webex.com</a>\n<img alt=\"leavingcisco.com\" height=\"18\" width=\"18\" src=\"http://www.cisco.com/images/exit.gif\" />.</p> \n  <p> </p> \n  <p> Cisco has released software updates that address these vulnerabilities.</p> \n  <p>This advisory is posted at\n\t <a style=\"WORD-BREAK:BREAK-ALL;\" href=\"http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20110201-webex\">http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20110201-webex</a>.\n\t </p> \n \n\n"
    }
]

例3) 2020年にリリースされた Security Impact Rating (sir) が Critical な PSIRT の数を調べる

openVulnQuery --year 2020 -f sir | grep -c "Critical"

出力結果
25

まとめ

このような形で 必要な PSIRT の情報を入手することができます。

運用中の多くのネットワーク機器の脆弱性情報を API を使うことで、まとめて取得できるため、活用いただけるシーンは多いのではないかと思います。今回は、コマンドラインでの例を中心に記載しましたが、Python ライブラリとしてプログラムの中に組み込むことで、より効率的な情報収集や、その他アプリケーションとの連携が可能です

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

pydubを使うためにdockerにffmpegをインストール

 djangoでwebアプリを作っている時に、m4a形式の音声をwav形式に変換するために、docker環境にpydubを入れようとしたのですが、pydubを使うためにはffmpegというossをインストールする必要があるとのことでした(pydubはffmpegのラッパーとのこと)。
 ところがffmpegの公式ページに行くとコマンドでのインストール方法が見つからなく、必要なコマンドをDockerfileに書けないなといった状況でした。幸い、githubで参考になる書き方を見つけたので、時間を溶かさずにすみましたが、忘れないように備忘録としてメモします。

Dockerfile
FROM python:3.8
ENV PYTHONBUFFERD 1
RUN mkdir /workspace
WORKDIR /workspace
COPY requirements.txt /workspace/
RUN apt-get update && \
    apt-get -y install ffmpeg libavcodec-extra && \
    pip install --upgrade pip && \
    pip install -r requirements.txt 
CMD ["python3","manage.py","runserver","0.0.0.0:8000"]
EXPOSE 8000

このようなDockerfileをbuildすることで、Docker環境で無事ffmpegをインストールし、pydubを使うことができました(requirement.txt内にpydubを書く)。

なお、参考にしたgithubのurlは下記です。
https://github.com/546669204/docker-pydub/blob/master/Dockerfile

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

雰囲気で使うMedic

一年ぶりまして。
今年もAdventCalendarの季節がやってまいりました!
今年のMaya Advent Calendarはというと例年と同様に大好評(?)のようで、安心しました(涙目)

さて、それでは今回はMedicという個人的に触ってるツールについてお話します。

Medicについて

Medicとは、Mayaで動作するオープンソースのデータチェックツールです。
Python、C++でテスト項目(Tester)を拡張でき、それらを一つのチェックリスト(Karte)として統合することでシーン全体または選択したノードに対して一括でデータチェックを走らせることができます。
(画像左:Karte画面、画像右:Tester画面)
image.pngimage.png

この記事では細かいことは置いておいて、TesterKarteというMedicの中でも重要な2つの要素について触れていきます。

Tester(テスター)

Testerはチェック項目本体です。
Testerは先述した通りPythonとC++で拡張できますが、私はC++ワカラナイのでここではPythonのTesterについて書きます。

Testerの書き方は基本的にここに書いてある情報しかありません。
(ググってみても第三者によるそれらしい情報のようなものは殆ど見つけられませんでした。)

これだけの情報だと少し心細さはありますが、Medicには最初からお手本となるような便利なTesterがそれなりのボリュームで用意されているため、それらのコードを読むことで大体の雰囲気を掴むことが出来ました。

実際のコードを見ていきましょう。

example1: anyLayer.py (code link)

image.png
これはシーン内にBaseAnimation, defaultRenderLayer, defaultLayerがある場合に検知するTesterです。
上画像の例では layer1 が存在するためエラー表示になっています。

このTesterのコードを例に、それぞれのブロックで何が行われているか解説していきます。

AnyLayer(medic.PyTester)

class AnyLayer(medic.PyTester):
    def __init__(self):
        super(AnyLayer, self).__init__()
    ...

Testerは必ずmedic.PyTesterを継承する必要があります。
このPyTesterの内容をオーバーライドすることで独自のTesterが出来上がるわけです。

Name

def Name(self):
    return "AnyLayer"

GUIのTester一覧画面に表示される文字列を設定する関数です。

Description

def Description(self):
    return "Check if any render, anim or display layer exists"

GUIに表示されるTesterの説明文を設定する関数です。

Match

def Match(self, node):
    return node.object().hasFn(OpenMaya.MFn.kDisplayLayer) or node.object().hasFn(OpenMaya.MFn.kAnimLayer) or node.object().hasFn(OpenMaya.MFn.kRenderLayer)

ここではTesterの対象となるノードの条件を記載します。
このコードの例ではkDisplayLayer, kAnimLayer, kRenderLayerがフィルタリングされます。

引数のnodeはMedic独自のMdNodeというノードが入っていて、node.object()と書くことでMObjectを呼び出しhasFunでノードのtypeを判定しています。

Medicではこのnodeが非常に重要になってきます。
nodeの中身は詳しく知りたい方はこちらのコードをご覧ください。

test

def test(self, node):
    if node.name() not in ["BaseAnimation", "defaultRenderLayer", "defaultLayer"]:
        return medic.PyReport(node)

    return None

Matchでフィルタリングされたnodeはこのtestでデータチェックが行われます。
このコードの例ではnode.name()でノード名の文字列を取得し、その文字列が"BaseAnimation", "defaultRenderLayer", "defaultLayer"以外であればmedic.PyReport(node)を返します。
この「medic.PyReport(node)を返す」という部分がエラーを検出したということになり、検出されたノードはGUIのListViewに一覧されます。
エラーが検出されなかった場合はNoneを返します。

fix

def IsFixable(self):
    return True

def fix(self, report, params):
    node = report.node()
    if node.dg().isFromReferencedFile():
        return False

    if node.dg().isLocked():
        node.dg().setLocked(False)

    cmds.delete(node.name())

    return True

Medicではtestで検出したエラーを修正(fix)するロジックを書くこともできます。
この処理はtestと違って実際のデータを弄ることになるため、もしfixを作成する場合は気を付けて書く必要があります。

コードの例ではまずreport.node()でtestに引っかかったnodeを取得し→
そのnodeがリファレンスだった場合は処理を中断しFalseを返す、リファレンスでない場合は→
そのノード(レイヤー)がロックされているかどうかを調べ、Trueならロックを解除、Falseならそのまま→
cmds.delete(node.name())でノードを削除し→
fix処理が完了

といった処理内容になっています。

IsFixbleはこのTesterがエラーを修正できる能力を持っているかどうかを表すためのもの(GUIのfixボタンをenableにするためのもの)なので、もしfixを書くならセットでIsFixbleをオーバーライドしてTrueを返しましょう。

Create

def Create():
    return AnyLayer()

いまいち詳しいことは分かっていませんが、とりあえずCreateでこのTester本体のclassのインスタンスを返す必要があるようです。
深く考えず、おまじないだと思って忘れずに書きましょう。

testの補足:GUIのListViewからコンポーネントを選択できるようにする

「transformノードの名前がルールと違うかどうか」を調べるようなTesterだった場合は問題のノードはどれか分かるだけで問題ないと思います。
しかし、「とあるメッシュの全頂点のウェイト値がルール通りかどうか(小数点第3位以下が切り捨てられてるか 等)」を調べるようなTesterの場合、問題のあるメッシュと合わせて問題のある頂点も選択できた方が便利だと思います。

実はMedicではそういった操作もサポートしています。
しかし、標準のTester、具体的にはpythonで書かれたTesterにはそういった処理がどれにも書かれていません。

それではどこに書かれているのかというと、C++で書かれたTesterの中に答えが書かれています。

少しだけコードを覗いてみましょう。

example2: nonManifoldVertex.cpp (github link)

このTesterのコードの37~66行目に書かれている内容がその答えになります。
一応コードを解説すると、

MItMeshVertex itvtx(node->getPath());

nodeの持つdagPathからMItMeshVertexイテレーターのitvtxを作成し、

MFnSingleIndexedComponent comp;
MObject comp_obj = comp.create(MFn::kMeshVertComponent);

MFnSingleIndexedComponent型のcompと、
comp.create(MFn::kMeshVertComponent)で生成したMObjectをもったオブジェクトcomp_objを生成し、

while (!itvtx.isDone())
{
    MIntArray faces;
    MIntArray edges;
    itvtx.getConnectedFaces(faces);
    itvtx.getConnectedEdges(edges);

    if ((edges.length() - faces.length()) > 1)
    {
        result = true;
        comp.addElement(itvtx.index());
    }

    itvtx.next();
}

全頂点をイテレーションして問題のある頂点がある場合はresultにtrueを代入すると同時に頂点のindexをcompに格納しています。

そして、resultがfalseであればreturn 0を返し、tureであればMdReport(Python TesterのPyReportに相当)を返します。

return new MdReport(node, comp_obj);

ここでMdReportの第2引数に注目すると、comp_objが渡されているのが分かると思います。
察しの良い人はお気づきかと思いますが、PyReportでも同様に第2引数に問題のあるコンポーネントのインデックスが格納されたMObjectを渡してあげるとGUIから問題のあるコンポーネントを選択できるようになります。

少し難点なのは、コンポーネントのインデックスが格納されたMObjectを生成する必要がある都合上、
OpenMayaをある程度理解して使える必要があることかなと思います。
今までcmdsやpymelで慣れてきた人にとっては少しハードルがあるかもしれませんが、頑張りましょう。

Testerまとめ

Testerとは要はMatchでテストしたいノードをフィルタリングし、testで実際にテストして問題を検出し、必要に応じてfixで修正するという役割を持ったモジュールです。

Medicにおいてもっとも重要な要素なので、Medicを使うなら是非マスターしたいですね。

Karte(カルテ)

Karteは先ほど説明したTesterをひとまとめにするためのものです。
例えばモデリング工程であればモデリング工程用のTesterをひとまとめにし、セットアップ工程ではセットアップ工程用のTesterをひとまとめにするなど、そういった用途で使います。

Karteの作り方はTesterと比べると非常にシンプルです。

example3: all.karte (github link)

これがall.karteの全文です。
名前から分かる通り全てのTesterを表示します。

{
    "Name": "All",
    "Description": "All testers",
    "Testers": ["*"],
}

jsonのような構文になっていて、これをもとに目的に合わせて内容を書き換えることでKarteを作成します。
例えば以下のようなKarteの場合、

{
    "Name": "Modeling",
    "Description": "Modeling testers",
    "Testers": [
        "NonManifoldEdge",
        "NonManifoldVertex",
        "NonUniqueName",
        "ZeroLengthEdge"
    ]
}

こういう感じになります。
image.pngimage.png

Karteまとめ

全ての工程、全てのプロジェクトですべてのTesterが必要になるかというとそういうわけではないと思います。
そういった場合のこのKarteを使用して、必要なTesterだけをまとめたチェックリストを作りましょう。

まとめ

データチェックツールの類は各社で独自に用意されていることが多いと思います。
ただ、0から作るとなるとツールそのものを用意したりUIを用意したりそれらを更新するシステムを作ったりとそれなりにコストがかかると思います。

そこで、Medicはこれからチェックツールを用意しようとしている場合に検討する候補の一つとしておすすめです。

データチェックという非クリエイティブな作業をとことん自動化して、クリエイティブな作業に費やす時間を増やしたいですね!

明日の記事はUnPySideさんの記事です。お楽しみに!

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

【調査中】Tkinter + CV2 + pyzbarでプログラムがクラッシュする

いきさつ

以前親戚の八百屋(?)に納品した出退勤システムをQRコード対応仕様にアップデートしようとコードを組んでいた所、何やらPythonがクラッシュする自体に陥った

この問題は?

進行中

利用中のPCについて

Macbook Pro 2019年モデル・CPU Intel Core i7・メモリ 16GB・ストレージ1TB(内200GBはBootCampでWindows領域に割り当て)

Dynabook T7(2019年夏モデル)・CPU Intel Core i7 8565U・メモリ 8GB・ストレージ 256GB+1TB デュアルストレージ(家電量販店オリジナルカスタムモデルのため機種のメーカー公式スペックと若干の相違あり)

やりたかったこと

QRコードを読み込み、もし「従業員」なら登録完了音声を鳴らし、もし「来訪者」として登録されていたら、登録完了音声とともにその来訪者とアポを取っている担当の人間の端末に通知する

その際、画面上にTkinterのmessageboxを利用し、「OO様、お待ちしておりました。只今担当の人間を呼び出しております」というような表示をするシステムを作りたかった

状況

QRコードを利用するシステムは作ったことがなかったので、まずは先駆者の方のコードを写経し、それをカスタムして雰囲気を掴み、本格的にアップデート用のプログラムを書いていく作戦にした。
そこで今回は、一番分かりやすかったこちらの方のコードを参考に、以下のようなコードに改造してみた。

qrtest.py
from pyzbar.pyzbar import decode
import cv2
from pygame import mixer
from tkinter import messagebox as mbox

systemonly = ""
cap = cv2.VideoCapture(0)
font = cv2.FONT_HERSHEY_SIMPLEX
while cap.isOpened():
    ret,frame = cap.read()
    if ret == True:
        d = decode(frame)
        if d:
            for barcode in d:
                x,y,w,h = barcode.rect
                cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
                barcodeData = barcode.data.decode('utf-8')
                if(systemonly != barcodeData):
                    mixer.init()
                    qrlist = barcodeData.split(",")
                    if(qrlist[1] != "Visitor"):
                        mixer.music.load("sound/recognized.mp3")
                        mixer.music.play(1)
                        systemonly = barcodeData
                    elif(qrlist[1] == "Visitor"):
                        mixer.music.load("sound/visitor.mp3")
                        mixer.music.play(1)
                        mbox.showinfo("Welcome",f"お待ちしておりました。{qrlist[0]}様。\n担当の者が向かいます。しばらくお待ち下さい。")
                        systemonly = barcodeData
        cv2.imshow('QRTEST',frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()

そして「来訪者QR」を読み込ませて発生したエラーが以下のようなもの。

2020-11-30 21:33:22.249 python[2161:55317] -[NSApplication macMinorVersion]: unrecognized selector sent to instance 0x7fb09c47c3a0
2020-11-30 21:33:22.251 python[2161:55317] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSApplication macMinorVersion]: unrecognized selector sent to instance 0x7fb09c47c3a0'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff204b76af __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x00007fff201ef3c9 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff20539c85 -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff2041f06d ___forwarding___ + 1467
    4   CoreFoundation                      0x00007fff2041ea28 _CF_forwarding_prep_0 + 120
    5   libtk8.6.dylib                      0x000000011ffe7db9 SetCGColorComponents + 265
    6   libtk8.6.dylib                      0x000000011ffe867a TkpGetColor + 250
    7   libtk8.6.dylib                      0x000000011ff22aa9 Tk_GetColor + 153
    8   libtk8.6.dylib                      0x000000011ff128e6 Tk_Get3DBorder + 134
    9   libtk8.6.dylib                      0x000000011ff1274f Tk_Alloc3DBorderFromObj + 127
    10  libtk8.6.dylib                      0x000000011ff23fad DoObjConfig + 941
    11  libtk8.6.dylib                      0x000000011ff23ae5 Tk_InitOptions + 357
    12  libtk8.6.dylib                      0x000000011ff239c5 Tk_InitOptions + 69
    13  libtk8.6.dylib                      0x000000011ff54b5c CreateFrame + 1548
    14  libtk8.6.dylib                      0x000000011ff54e37 TkListCreateFrame + 151
    15  libtk8.6.dylib                      0x000000011ff4c7f8 Initialize + 2168
    16  _tkinter.cpython-38-darwin.so       0x0000000115e7768b Tcl_AppInit + 91
    17  _tkinter.cpython-38-darwin.so       0x0000000115e7734e Tkapp_New + 590
    18  _tkinter.cpython-38-darwin.so       0x0000000115e770ee _tkinter_create_impl + 222
    19  _tkinter.cpython-38-darwin.so       0x0000000115e76bd6 _tkinter_create + 182
    20  python                              0x0000000109e17644 cfunction_vectorcall_FASTCALL + 84
    21  python                              0x0000000109ef0f20 _PyEval_EvalFrameDefault + 45072
    22  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    23  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    24  python                              0x0000000109db97dd _PyObject_FastCallDict + 93
    25  python                              0x0000000109e3ff52 slot_tp_init + 178
    26  python                              0x0000000109e4f4c1 type_call + 289
    27  python                              0x0000000109db9ad7 _PyObject_MakeTpCall + 167
    28  python                              0x0000000109ef13d1 _PyEval_EvalFrameDefault + 46273
    29  python                              0x0000000109dbb0e8 _PyFunction_Vectorcall + 248
    30  python                              0x0000000109ef0f20 _PyEval_EvalFrameDefault + 45072
    31  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    32  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    33  python                              0x0000000109ef0f20 _PyEval_EvalFrameDefault + 45072
    34  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    35  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    36  python                              0x0000000109db97dd _PyObject_FastCallDict + 93
    37  python                              0x0000000109e3ff52 slot_tp_init + 178
    38  python                              0x0000000109e4f4c1 type_call + 289
    39  python                              0x0000000109db9ad7 _PyObject_MakeTpCall + 167
    40  python                              0x0000000109ef13d1 _PyEval_EvalFrameDefault + 46273
    41  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    42  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    43  python                              0x0000000109ef0e2b _PyEval_EvalFrameDefault + 44827
    44  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    45  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    46  python                              0x0000000109dba4a9 PyVectorcall_Call + 121
    47  python                              0x0000000109ef1829 _PyEval_EvalFrameDefault + 47385
    48  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    49  python                              0x0000000109dbb19a _PyFunction_Vectorcall + 426
    50  python                              0x0000000109ef0f20 _PyEval_EvalFrameDefault + 45072
    51  python                              0x0000000109ee412d _PyEval_EvalCodeWithName + 557
    52  python                              0x0000000109f5c346 PyRun_FileExFlags + 358
    53  python                              0x0000000109f5ba91 PyRun_SimpleFileExFlags + 529
    54  python                              0x0000000109f842df pymain_run_file + 383
    55  python                              0x0000000109f839ab pymain_run_python + 523
    56  python                              0x0000000109f83745 Py_RunMain + 37
    57  python                              0x0000000109f84e11 pymain_main + 49
    58  python                              0x0000000109d8d768 main + 56
    59  libdyld.dylib                       0x00007fff20360631 start + 1
    60  ???                                 0x0000000000000002 0x0 + 2
)
libc++abi.dylib: terminating with uncaught exception of type NSException
zsh: abort      python ReleaseObserver.py

おまけにMacからもGUIで警告が表示されている。こんな感じの↓
スクリーンショット 2020-11-30 21.10.41.png

こんな親切に出してくれているのに、同じエラーが出ている人間が殆ど居ない上に原因がわからないしmessageboxを表示させなければエラーが出ないものだから、段々と腹が立ってきた。
実は似たようなエラーはゴロゴロと転がってはいるものの、どれも私のMacで発生したエラーとは異なる。
私のエラーの2行目、この部分↓

'-[NSApplication macMinorVersion]: 

大抵同じようなエラーが発生している人は、同じ部分がこの様になっている↓

'-[NSApplication _setup:]:

macMinorVersionというエラーは相当珍しいということを発生件数が物語っている。

いろいろ試した。MatplotlibとTkinterが喧嘩してるのではとか(そもそも使ってないけど、内部的に似たようなもの使っているのかも?という淡い期待を込めて)、tkaggがどうとか、とにかく目に入るものすべてを試したが動作せず。

当たり前。エラーがそもそも違うのだから、対処法も違うに決まっている。

仮説

上のエラーにはmacMinorVersionという文言が表示されている。

私はちょっぴりこれに対して心当たりがある。

というのも、11月12日(日本時間では確か13日)、リリース日当日にMacOSをBigSurにアップデートしているのだ。

別に仕事じゃないし、どうせ不具合なんか起きてもCatalinaに戻せばいいや〜、なんて軽く考えていた。

もしかしたらこれが原因かもしれない。

実験

幸いにも、私はWindowsマシンの「Dynabook」を所有している。

今回はこのDynabookを用いて検証していこう・・・と思ったが、WindowsではPyzbarがバグって動作しない。現在こちらの問題も調査中。

何がしたかったか

とりあえずまずこんなエラーが出ているということを共有したかった。

解決策をご存じの方は是非教えて下さい。

引き続きこの問題を調査していきます。

何か分かったら追記します。 Pスクリーンショット 2020-11-30 21.10.41.png
スクリーンショット 2020-11-30 21.10.41.png

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

Ramp制御を含むGUIを作る

この記事はMaya Advent Calender 2020の3日目の記事です。
AdventCalenderに初めて参加させていただきます。

はじめに

Rampのカーブを入力として利用したツールを制作する為に調べました。
今回は、PySideではなくmayaコマンドでGUIを作っています。

GUI.png

サンプル

下記のコードを実行すると、GUIが表示されます。

import maya.cmds as cmds
import maya.mel as mel

class rampWindow(object):
    @classmethod
    def showUI(cls):
        """
        showUI
            UIを表示させます。
        """ 
        win = cls()
        win.create()
        return win

    def deleteUI(self, *args):
        """
        deleteUI
            UIを削除します。
            rampアトリビュート用のNullを削除します。
        """ 
        cmds.deleteUI(self.window, window=True)
        self.deleteRampNode()

    def deleteRampNode(self, *args):
        """
        deleteRampNode
            rampアトリビュート用のNullを削除します。
            ウィンドウの×を押した時のコマンド用。
        """ 
        if cmds.ls(self.nodeName):
            cmds.delete( self.nodeName )

    def __init__(self):
        """
        __init__
            初期化。
        """ 
        self.window = 'rampWindow'
        self.title = 'rampWindow'
        self.width=200

        self.rampName = "ramp"
        self.nodeName = "rampNull"

    def create(self):
        """
        create
            レイアウト作成。
        """ 

        #   多重ウィンドウ対応
        if cmds.window(self.window, exists=True):
            self.deleteUI()

        #   rampアトリビュート用Nullを作成
        cmds.group(em=True, n=self.nodeName)
        createRampAttrbute( self.nodeName, self.rampName )

        #   ウィンドウ作成
        self.window = cmds.window( self.window, t=self.title, w=self.width, cc=self.deleteRampNode )
        cmds.columnLayout()

        #   Layout Frame Range
        rampLayout( self.nodeName, self.rampName )

        cmds.showWindow(  )

def createRampAttrbute( nodeName, rampName ):
    """
    createRampAttrbute
        RampAttrbuteを設定します。   
    Args:
        String nodeName  :   RampAttrbuteの追加するノード名
        String rampName  :   RampAttrbute名
    """
    cmds.addAttr( nodeName, ln=rampName, at='compound', nc=3, m=True)
    cmds.addAttr( nodeName, ln='ramp_Position', at='float', p=rampName, dv=-1)
    cmds.addAttr( nodeName, ln='ramp_FloatValue', at='float', p=rampName, dv=-1)
    cmds.addAttr( nodeName, ln='ramp_Interp', at='enum', en='None:Linear:Smooth:Spline', dv=1, min=0, max=3, p=rampName)
    cmds.setAttr( '{0}.{1}[0]'.format(nodeName, rampName), 0, 1, 2)
    cmds.setAttr( '{0}.{1}[1]'.format(nodeName, rampName), 1, 1, 2)

def rampLayout( nodeName, rampName ):
    """
    rampLayout
        rampをレイアウトします。
    Args:
        String nodeName  :   RampAttrbuteの追加されているノード名
        String rampName  :   RampAttrbute名
    """
    mel.eval('if (! exists("AEmakeLargeRamp")) source AEaddRampControl')
    mel.eval('AEmakeLargeRamp("{0}.{1}", 0, 0, 0, 0, 0)'.format(nodeName, rampName))


if __name__ == '__main__':
    rampWindow.showUI()

解説

  • レイアウトを行う前に、GUIで使用するRampのパラメータを保持する為にNullを用意しています。 そのNullにRampアトリビュートを追加します。
#   rampアトリビュート用Nullを作成
cmds.group(em=True, n=self.nodeName)
createRampAttrbute( self.nodeName, self.rampName )

ramp2.png

  • ウィンドウを用意し、Rampをレイアウトします。
mel.eval('if (! exists("AEmakeLargeRamp")) source AEaddRampControl')
mel.eval('AEmakeLargeRamp("{0}.{1}", 0, 0, 0, 0, 0)'.format(nodeName, rampName))
  • AEmakeLargeRamp コマンド。引数が6個必要ですが、rampのアトリビュート名を指定すれば、後は0で問題ないと思います。
AEaddRampControl.mel
global proc AEmakeLargeRamp( string $nodeAttr,
                            int $bound,
                            int $indent,
                            int $staticEntries,
                            int $staticPositions,
                            int $adaptiveScaling )

おまけ

実行ボタンを起動しランプの値をとる際には、Fit関数を用意すると便利です。
この関数で、パラメータの「開始値1・終了値1」間の変化を、「開始2・終了2」間の変化にマッピングします。
HoudiniのVEXライクな関数を作ってみました。

def fit(parameter, startValue1, endValue1, startValue2=0, endValue2=1):

    """
    fit
        値の範囲を別の値にマッピングする。
    Args:
        Float parameter  :   パラメータ
        Float startValue1  :   開始値1
        Float endValue1  :   終了値1
        Float startValue2 :   開始値2
        Float endValue2  :   終了値2
    Returns:
        Float   value  :   マッピングされたの値
    """ 

    if parameter < startValue1:
        parameter = startValue1
    if parameter > endValue1:
        parameter = endValue1

    Span1 = endValue1 - startValue1
    Span2 = endValue2 - startValue2

    rate = float( parameter - startValue1 ) / Span1
    value = startValue2 + ( rate * Span2 )
    return value

参照

ryusas/test_falloffCurveAttr.py
https://gist.github.com/ryusas/699ad3f5fa8469a00e8df4b3672aa89f


Maya Python Advent Calender 2020の4日目は、it_ksさんの記事です!!

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

Pythonで〇〇が立った!〜scipyで慣性モーメント最小化問題を解く〜

teratailの質問回答で、図形が立つとは何ぞや、という質問に答えてみましたので、一般化して記録しておきます。

皆さんおなじみの左のような絵、走っている状況であるのと漫画風のデフォルメが効いているので、自然に感じます。しかし、真ん中のように2値化してみると、漫画効果が薄れて、なんだかバランスが悪く、倒れてしまいそうに感じます。それを、右のように「立たせる」ための角度を、理論的に求めて、立たせてみよう、という試みです。

image.png

アイデアは次のようなものです。

  • まずは図形の重心が中心になるように並行移動する
  • そのあと、中心を軸に回転させて、最も「バランスよく立った」状態にする
  • 「バランスよく立った」を、中心縦軸を固定して「まわした」時の慣性モーメントが最小になる、と定義する

慣性モーメントを最小化することで、左右をなるべく縮めた位置が求められます。結果的に図形が「立つ」ことになります。フィギュアスケートの選手が、腕をたたむことで、安定して高速回転できるのと、原理は同じです。

重心を求めること、並行移動、回転、目的関数の最小化にはPythonの数値計算ライブラリであるscipyの関数を使いました。scipyの勉強にもなりますね。目的関数である慣性モーメントを算出する部分のみnumpyで手作りしました。なお、上の図はレイアウトの関係上縦長の枠にしていますが、実際に計算した図形は、回転してはみ出さないように、ほぼ正方形を枠としています。

以下がコードです。-14.12度ほどの回転をさせることで、気持ちよく「立たせる」ことができました。

import numpy as np
from scipy import ndimage, optimize
import matplotlib.pyplot as plt
import cv2

# 画像読み込みと2値化、白黒反転
img = cv2.imread('sample.jpg', 0)
_, img = cv2.threshold(cv2.bitwise_not(img), 1, 1, cv2.THRESH_BINARY)

# 平行移動して重心を中心にする
center = ndimage.center_of_mass(img)
img = ndimage.shift(img, np.array(img.shape)/2-np.array(center))

# 2値化した図形を表示
plt.imshow(img)
plt.show()

# 図形を立てた時の、中心軸に対する慣性モーメント
# mass:縦軸ピクセル数、radius:中心軸からの左右のピクセル距離
# 慣性モーメント = Σ mass * radius^2
def inertia(img):
    mass = img.sum(axis=0)
    radius = np.abs(np.arange(-len(mass)//2, len(mass)//2+1))
    if len(mass) != len(radius):
        radius = radius[radius != 0] - 0.5  # 中心位置の補正
    return (mass * radius * radius).sum()

# 回転させて慣性モーメントを測定する=最小化関数
def rotated_inertia(degree, img):
    return inertia(ndimage.rotate(img, degree, reshape=False))

# 最小化関数の最小値を求めることで、-90〜90度の回転時の最小慣性モーメントを得る
res = optimize.minimize_scalar(
    rotated_inertia, bounds=[-90,90], args=(img), method='Bounded')

# 最小値の時の回転角度を求める
print(res.x)

# 最小値の時の回転を実際に行う
img = ndimage.rotate(img, res.x, reshape=False)

# 「立った」図形を表示
plt.imshow(img)
plt.show()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scikit-learnのr2_score関数の取りうる範囲に関するメモ

はじめに

scikit-learnでR^2を計算した結果をそのまま出力していたのだが、R^2にしてはマイナスの値が大きくない?というつっこみがあったのであらためて調べてみた。

scikit-learnのR^2(r2_score関数)定義

ソースを見てみる。
https://github.com/scikit-learn/scikit-learn/blob/0fb307bf3/sklearn/metrics/_regression.py#L499

重みなどいろいろ複雑な処理がはいっているが、定義はよくある以下の数式のようだ。

1 - \sum{(y-\hat{y})^2} / \sum{(y-\bar{y})^2} 

ちなみに、データ数が1の場合、定義からも分かるように、後ろの項の分母が0になるため計算できない。

とりうる値

では、このr2_scoreのとりうる値について考えて見よう。
最大値は定義から1である。
最小値はということ、$y$ と$\hat{y}$ の差を大きくすれば、後ろの項の分子はいくらでも大きくできるため、ものすごく大きなはずれ値があれば、いくらでもマイナスになりそうだ。

やってみよう

まずは同じ値を持つ者同士を引数に。

>>>from sklearn.metrics import r2_score
>>>r2_score([1,2,3,4,5],[1,2,3,4,5])

1.0

ちょっとだけ変えてみる。

>>> r2_score([1,2,3,4,5],[1,3,3,3,5])
0.8

全然違うものにしてみる。
おっと、いきなりマイナス。

>>> r2_score([1,2,3,4,5],[5,4,3,2,1])
-3.0

めちゃででかいはずれ値を入れる

>>> r2_score([1,2,3,4,5],[100,4,3,2,1])
-981.5

なるほど、いくらでもマイナスはとれそうだ。

おわりに

これで次回から「scikit-learnの関数の仕様です」と胸をはっていえそうだ。

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

Pythonで掃除Botを作成

Pythonで簡単な掃除Botを作成してみた!

経緯

弊社では週の仕事が最後の日に掃除を行います。
僕はその場所決めの担当をしています。
いつも集合してトランプを引いてもらい場所を決めるという作業が非常に効率が悪いと思っていました。
そこで今回はPythonを使用して開発しました!

手順

従来

①定時になる
②集合する(なかなか集まらない)
③トランプを引く(この作業もなくす)
④担当掃除場所へ行く

改善

①定時になる
②僕が
 コマンドプロンプト(Windows10)で実行
 実行結果をチャットツールに貼る
③集合せずにそのまま担当掃除場所へ移動

コード

import random
# メンバーを随時追加して下さい
women = ['woman1']
members = ['man1', 'man2', 'man3', 'man4', 'man5', 'man6', 'man7', 'man8', 'man9']
if 'man1' in members:
    print('喫煙所の掃除担当は、○○' )
    members.remove('man1')
elif 'man2' in members:
    print('喫煙所の掃除は、△△')
    members.remove('man2')
random.shuffle(members)
print('男性用お手洗い掃除担当は、' + members.pop() + 'と' +members.pop())
members.extend(women)
random.shuffle(members)
# 3人減らしたmemberを取得する
print('会議室担当は、' + members[0] + 'と' + members[1] + '\nゴミ収集担当は、' + members[3] +
    '\n掃除機担当は、' + members[4] + '\n水やり担当は、' + members[5] + '\nシュレッダーと三角コーナー担当は、' + members[6] + '\n宜しくお願いします!')
# + '\n掃除休みは' + members[7] + 'と' +member[8]

ぜひ参考にしてみて下さい!

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

社会人2年目がデータ分析で得た教訓

はじめに

こんにちは.
NTTドコモサービスイノベーション部で働いております社会人2年目の橋本です.

業務では機械学習を中心にコードを書いています.
社会人2年目もあと残すところ4ヶ月弱となりましたが,社会人1年目から今日までデータ分析業務にて突っ走って来ました.

そんな私がこれまでを振り返るとスタート時にこれを知っておけばもっとスムーズにデータ分析を進めれただろう感じるポイントがいくつかあります.

アドベントカレンダー2日目の今日はそんな私からのクリスマスプレゼントということで「社会人2年目になった私が感じるデータ分析業務の教科書」を皆様にプレゼントしようと思います!

           christmas_santa_elf.png

※これ以降に出てくる内容は,難しい技術の内容は出てきません.
ただエンジニアとして働く上では大切なことも含まれていると思います.
データ分析業務に従事している方や,それ以外の技術者の方,
多くの方に読んでいただいて「わかる!あるあるだよね」と思っていただけたら幸いです.

  

ちなみにこれから書くことはまだ実践中で,完全にできている訳ではないです.
(わかっていても中々すぐにできるようにならないというのが私の所感です.)

教訓その1.超重要!コードは誰が見てもわかるように書くべし

そもそもコードをあえて汚く書こうと思っている人なんていませんよね.
ただ何も意識しないで書くと,挙動がすぐに理解できないコードだったりすることはよくあります.

特に私の場合は
試行錯誤のフェーズ(データの中身を軽く確認する・どうやってコードに落とし込むか考える段階のこと)
でできたコードは読みにくいことが多いです.

ここでは,
qiita_woman_computer.png
ことの大切さをお伝えしたいです!

1.1 なぜコードを綺麗に書くか?

業務をしていてたまに起こるのが,

qiita_hatena_woman.png

みたいな状況です. この状況は割と深刻な状況です.
過去の自分は他人のようなものなので,
自分が読みづらいと思ったコードは他人も読みづらいです.

例えばデバッグしてもらいたいと思って第三者にコードを見せると


などの弊害が生まれてしまいます.

1.2 どうすればコードが綺麗に書けるか?

じゃあどうすれば読みやすいコードになるの?という疑問が湧いてくるかと思います.

詳細については,参考書籍:リーダブルコードを読むのがお勧めですが,

いくつか参考に私が実践していることを書いておきます.

  1. コメントでどんなプログラムか書く
    データ分析をする際はjupyter-notebookを使用することが多いです.
    精度向上のためにmodelを複数作る場合などがありますが
    後でコードを見るとどんな特徴量を付け足し・削除をしたのか一々コードを確認しないといけなかったり,
    なぜその特徴量を付け足したかなどの自分の思考プロセスを思い出せないことがあります.
    このようなことを避けるため,一番上のセルになぜ・どんなことをしたのか書いておいて識別できるようにしています.
  2. なるべくネストは使わない
    試行錯誤的にコードを書いていくと,過去の私が作成したif文の中に更にifを分岐させて…みたいな状況がよくあります.
    作業している私からするとごく自然な流れでifが二重になるコードを生成しているのですが,
    このようにしてコードを書くと未来の私がとても苦しみます.

  3. ドキュメントを書く
    どのコードがどんな動きをするコードなのか書いてまとめましょう.
    ディレクトリの構造も書いておいた方が後々の自分が助かります.

社会人1年目の頃はコードは試行錯誤のフェーズで書いたらそのままにしてしまうことが多かったですが

試行錯誤のフェーズから相手にコードを見せるフェーズに移る前にコードをわかりやすく書き換えるのが基本です.

大切なことは「とにかく一目見たらどんなコードがなんとなくわかるコードにする」ことです.

※勿論試行錯誤のフェーズでそのままにしていたのは,サボっていた訳ではなく
コードを綺麗にすることの価値がきちんとわかっていなかったので,別のことに時間を使ってしまっていた.というのが正しいです.
ただ時間はかかってもコードをわかりやすく整理しておくことはかなり重要なので
生産性向上のため絶対にやったほうが良い
です.

教訓その2.メモリや計算時間には気を遣うべし

データ分析をしていると,必然的に大きなデータを扱うことが多くなってきます.

qiita_atama_woman.png

などに悩ませられることがあります.

2.1 tqdmを用いて処理時間を目視で確認しよう

まずforループを回すときや,大きなデータを読み込むときには
tqdmを用いてプログレスバーを表示させるようにしましょう.

tqdm は進捗を目視で確認できる便利なモジュールです.

from tqdm import tqdm
for i in tqdm(range(100)):
    ...

このような形で簡単にforループ内に組み込むことができます.

100%|██████████| 100/100 [00:00<00:00, 273066.67it/s]

余談ですが,

2.2 適材適所のデータ構造を使うべし

python にはlistやdict,setなど様々なデータ構造があります.

普段pythonを使い慣れている方でも,きちんとドキュメントを読んだことがない方も居るかもしれません.
一度目を通してみると新しい知見があって面白いです.

業務ではlistを使用してしまうことが多いですが,
listだと処理が重く,遅くなってしまうことも多いのでsetなどが使えないかどうか考えてみると良いです.

小ワザとして,listを用いてappendを行う場合は
forベタ書き→リスト内包表記を行うことで多少は高速になります.

また,大きな疎行列などを扱う際にはscipyを使うなど工夫をしてあげると良いです.

2.3 chunksizeを使って大きなファイルを読み込む

あまりにもファイルが大きくて

df = pd.read_csv('img_data.csv')

このような形で読み込んでも時間がかかる場合は

df = pd.read_csv('img_data.csv',chunksize='500')

のようにしてファイルを読み込むと良いです.

教訓その3.最強のチームを目指し個人のスキルを向上すべし

データ分析の仕事は,比較的属人化しやすいです.
そのため,個人のスキルが高ければそれで充分だと感じる人も多いかもしれません.

3.0 属人化って何?

属人化という言葉を知らない人のために,念のため記載しておきます.
weblio より抜粋

企業などにおいて、ある業務を特定の人が担当し、その人にしかやり方が分からない状態になることを意味する表現。多くの場合批判的に用いられ、誰にでも分かるように、マニュアルの作成などにより「標準化」するべきだとされることが多い。企画・開発業務など、属人化されているのが一般的と言われる業務もある。

上記にあるように,属人化という言葉は批判的に用いられることが多いようです.
あくまで個人の見解ですが,属人化と標準化はバランスが大切であり,どちらに傾きすぎても良くないと考えています.

           

3.1 なぜ属人化しやすいの?

データ分析の仕事は,仕事の性質上属人化が起こりやすいと考えています.

データ分析の大まかな流れは,
1. ビジネス課題からデータ分析できる課題に落とし込む
2. どんなデータが使えるか考える
3. データ基礎分析
4. 特徴量生成
5. モデル作成
6. 精度確認
7. 精度向上のため4.5.6.を繰り返す

このような一貫した流れで仕事をすることが多いです.

基本的に上記のタスクを一人でこなしていくことが多いと思います.
1.2.の部分は比較的チーム内で相談しやすいかなと思いますが
3.以降はやはり一つのデータについて詳しくなっていってしまうので,一人で行うことが多くなります.

3.2 属人化のメリット・デメリット

上述したように,属人化と標準化は対義語にあたるものですが,どちらかであればいいというものではなくバランスがとても大切です.

属人化のメリット

  1. 自分のペースで働くことができる
  2. 責任感をもち一人一人が働く
    タスクが個人に紐づいているので,
    標準化で起こりがちな”だれかにタスクをなすりつける人が出てくる”ということは少ないです.

属人化のデメリット

  1. 異動などによるチームメンバーの欠如によってアウトプットに大きな差が出てしまう
    チームのメンバーによってアウトプットが異なってしまうので,割と致命的な属人化のデメリットです.
    できるだけアウトプットに差が出ないように対策をしておいた方が良いと思います.
  2. 引継ぎに時間がかかる
  3. ミスに気付きにくい
    社会人1年目の時に特に不安だったことの一つです.
    自分が何かミスしても自分しかわからないのでいつも最悪の場合を想定し,慎重に作業は行っています.
  4. きちんと作業しているか不透明
    タスクが個人で紐づいているので,他人の作業については何をしているかよくわからないこともありますが
    基本は相手を信用して作業をしているだろうと思っています.

3.3 どんなスキルが必要か?

タスクの流れは3.1でなんとなくわかったかなと思うので,
3.2ではデータサイエンティストとして必要なスキルを紹介します.
業務で必要なスキルは割と幅広く,
社会人として他の業種でも必要な論理的思考力やプレゼン力,タイムマネジメントから
専門的なものまであると感じています.
データサイエンティストはコーディングについては
python(特にPandasなど)やSQLなどを用いることが多いです.
深層学習も扱うので深層学習系のライブラリも使えると良いかなと思います.

データサイエンティスト協会から
スキルチェックリスト

なども出ているようですのでぜひ参考にしてみてください.

3.4 チームで働く意義

教訓その1で書いたようにチーム内でコードを見せ合うこともありますし,
それぞれ持っているバックグラウンドが少しずつ異なるので,詰まった時にアドバイスをもらうことも多いです.

またチーム内では,急病や異動などによって急遽人が抜けてしまうことが考えられます.
その際にあるタスクを一人でこなしていて,必要なドキュメントやコードだけ置いていかれてしまうよりも,
日頃からチーム内でのメンバーである程度やっている内容やコードなどを理解していた方が
メンバーが欠如した際の立て直しが早く,
僅かではありますが属人化を防ぐことができます.

またチームで仕事をすると,様々な考え方やスキルを持った人が増えるので
一人だけで作業をするよりも良いアウトプットを出せるようになると考えられます.

教訓その4. 新しい技術のキャッチアップをすべし

4.1 なぜキャッチアップするの?

キャッチアップが必要な理由は大きく分けて二つです.

  1. 技術の進化スピードが速い
    機械学習の分野は,技術の進化のスピードがとても速いので,新しい技術のキャッチアップは欠かせません.
  2. チーム内での個性の確立
    3章でもチームについて触れましたが,
    チームで仕事をこなす際には,多様性が重視されます.
    これはデータサイエンティストなどの括りではなく,世間一般的にそうであると考えます.

       

考え方のみでなく,スキルセットも多様性に富んでいるほうが
チームはより素晴らしく洗練されたものになります.

3章で述べた基本的なスキルセット+αによって個人の地位を確立すると
チーム内での存在意義を感じられて仕事をしやすくなります.

しかし新入社員や経験が浅い人が,すぐにチームで個性を発揮できるほど,
ある一つの分野に精通しプロフェショナルになるのは難しいです.

もちろん先輩と同じ分野で先輩以上のスキルを身に付けられるのであれば何も問題ないですが,
そうなるには膨大な努力と時間が必要になります.

そこで,新しい分野を取り入れるという方法が考えられます.

新しい分野であれば,先輩社員であってもあまり知らない部分も多く
先輩と同様に地位を確立できるチャンスを掴むことができます.

このように,最新技術をキャッチアップし,詳しくなることで自分だけでなく,
チームとしても最強になれるのです.

4.2 キャッチアップの方法と最新技術の導入・検証について

キャッチアップの方法は人それぞれだと思います.
どの方法が正しいというのはないと思うので好きな方法で行えばいいと思っていますが,
大きな学会で発表された論文などは手堅くチェックしておくと良いかなと思います.
Google Scholarなどを用いて検索するのも良いかと思います.

良さそうな最新技術を見つけたら,実際にその技術が活かせるか検証してみましょう

技術の導入検証は,つまづくことが多いです.
先輩も知らないことを取り入れようとしているので
わからない時にどうしたら良いか困ってしまうこともあるかもしれません.

お勧めの方法は
1. 公式ドキュメントを参考にする
2. 公式のQuestion&Answerを参照する
3. 公式のQuestion&Answerで質問する

この流れで行っていくと良いです.
(先輩曰く大抵のことはドキュメントで解決できるらしいですが
私はドキュメントと公式のQuestion&Answerを行ったり来たりして少しずつ知見を深めていくのが好きです.)
どうしてもわからない時は,公式のQuestion&Answerで聞きましょう.

※質問は英語ですることが多いですが,英語が苦手な技術者であることも想定して,
わかりやすい英語で書くか,コードを貼り付けたりすると良いと思います.

最後に

いかがでしたでしょうか?

私がデータ分析をする中でつまづいた部分とそこから得られた教訓について大まかに書きました.
社会人2年目までに感じた葛藤や,つまづいた部分をできるだけわかりやすく書いたつもりです.
これからデータサイエンティストを目指す方,エンジニアとしてコードを書く方,チームで仕事をする方など幅広い人に読んで貰えたら嬉しい限りです.

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

Platypus(遺伝的アルゴリズム)で整数と実数を一緒に使う魔法の呪文

Platypusで整数と実数を同時に使うと

Platypusで、整数を使う時はInteger、実数を使う時はRealを使って設定をしますが、両方を一緒に使うと次のエラーが出ます。

PlatypusError: must define variator for mixed types

なんか、variatorを定義しなさいと言うのですが、日本語のサイトには情報がありません。皆さんどちらかだけを使った例を書いています。
でも、実際には、整数と実数が混ざって使いたいことはよくあると思うのですが、エラーが出てどうしようもありません。

Platypusに書く魔法の呪文

いろいろ探していたら元々のGithubの英語のサイトに答えがありました。

https://github.com/Project-Platypus/Platypus/issues/31

いろいろ書いてあるのですが、たぶんこれをalgorithmの中に書くだけで動くようです。
variator=CompoundOperator(SBX(), HUX(), PM(), BitFlip()

これで、整数と実数が使えるようになります。

このプログラムは、そこのサイトにあったプログラムです。

from platypus import *

def mixed_type(x):
    print("Evaluating", x)
    return [x[0], x[1]]

problem = Problem(2, 2)
problem.types[0] = Real(0, 10)
problem.types[1] = Integer(0, 10)
problem.function = mixed_type

algorithm = NSGAII(problem, variator=CompoundOperator(SBX(), HUX(), PM(), BitFlip()))
algorithm.run(10000)

print("Final solutions:")
for solution in unique(nondominated(algorithm.result)):
    print(solution.variables, solution.objectives)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのimport内部実装 〜複雑怪奇なimportlib内部実装へようこそ〜

Pythonその2 Advent Calendar 2020 8日目の記事です。よろしくおねがいします。

記事の趣旨と対象読者

Pythonのプログラムを書くときに必ず登場するのが、以下のようなimport文です。

import numpy as np

一見、JavaScript ES Modulesにおけるimport文

import randomSquare from './modules/square.js';

に似ているようにも思われるのですが、実はPythonにおけるimport文は、Pythonがインタプリタ言語であることと相まって、他の言語と比べて非常に複雑な動作をします。その仕組みを、CPythonインタプリタや標準ライブラリのソースコードまで遡って調べるのがこの記事の趣旨です。

この記事は以下のような人には役立つかもしれません。

  • CPythonの内部実装に興味がある人
  • プログラムの動作を詳しく調べる方法を知りたい人
  • Python標準のモジュールローダを上書きして、独自のモジュールローダを実装したい人

また、以下のキーワードについても記事中で説明します。

import文, __file__, _import(), sys.path, sys.path_hooks, sys.meta_path, sys.sys.path_importer_cache

注意 本文中に掲載するコードは、わかりやすさのため変更を加えています。

解説の流れ

本稿では、実際のCPythonでのimport文の実行の様子を、以下の流れで解説します。

  • CPythonのコンパイル部分(ソースコードからバイトコードの生成)
  • CPythonのバイトコード実行部分
  • Pythonで記述された関数__import__の実行部分
  • __import__がモジュール名解決のために使用するFinder, Spec, Locatorについて

import

Pythonでは、importを用いることにより、モジュールを読み込むことができます。

import my_module

モジュールの多くはファイルを実体に持ちます。(ただし以下に述べるように例外もあります。)例えば上の例では、現在のファイルと同じディレクトリ(REPL環境の場合は作業ディレクトリ)又はsys.pathに示されたディレクトリにmy_module.pyというファイルがあるか、my_moduleというディレクトリがありその中に__init__.pyというファイルがあります1。以下のように確認できます2

import my_module
print(my_module.__file__)
# "/path/to/my_module.py" もしくは "/path/to/my_module/__init__.py"

なお、モジュールがファイルmy_module/__init__.pyで定義されている場合は、パッケージと呼ばれます3。その場合、ディレクトリmy_module/内にファイルmy_sub.pyを配置することで、サブモジュールとして扱うことができます。

# import my_module は書かなくても自動で実行される
import my_module.my_sub
print(my_module.__path__) # "/path/to/my_module"
print(my_module.__file__) # "/path/to/my_module/__init__.py"
print(my_module.my_sub.__file__) # "/path/to/my_module/my_sub.py"

コマンドラインから直接実行されるモジュールは"__main__"という名前になります。一度読み込まれたモジュールはsys.modulesに辞書形式で格納されるため、以下のようにすればコマンドラインから実行したファイル名を取得できます。

import sys
print(sys.modules['__main__'].__file__)

import文の内部実装

バイトコードの生成

import文はCPythonによってどのように実行されるのでしょうか。
まず、CPythonは入力されたPythonスクリプトを抽象構文木に変換します。その後、抽象構文木を解釈しPythonの内部バイトコードに置き換えます。
Python/compile.c:compiler_import()において、import文を表現する抽象構文木からバイトコードを生成しています。

注意 実際は、compiler_import以外にも、compiler_from_importが存在します(それぞれ、import x, from x import yに相当)。ここではcompiler_importのみを考えます。

Python/compiler.c
static int compiler_import(struct compiler *c, stmt_ty s)
{
    Py_ssize_t i, n = asdl_seq_LEN(s->v.Import.names);
    for (i = 0; i < n; i++) {
        alias_ty alias = (alias_ty)asdl_seq_GET(s->v.Import.names, i);
        int r;
        ADDOP_LOAD_CONST(c, _PyLong_Zero);
        ADDOP_LOAD_CONST(c, Py_None);
        ADDOP_NAME(c, IMPORT_NAME, alias->name, names);
        if (alias->asname) {
            ...
        }
        else {
            identifier tmp = alias->name;
            Py_ssize_t dot = PyUnicode_FindChar(
                alias->name, '.', 0, PyUnicode_GET_LENGTH(alias->name), 1);
            if (dot != -1) {
                tmp = PyUnicode_Substring(alias->name, 0, dot);
                ...
            }
            r = compiler_nameop(c, tmp, Store);
            ...
        }
    }
    return 1;
}

stmt_ty *sはInclude/Python-ast.hで宣言された構造体で、構文木を表します。simport文を表す場合、s->Importに構文木の情報が入力されています。

Include/Python-ast.h
typedef struct _stmt *stmt_ty;
struct _stmt {
    enum _stmt_kind kind;
    union {
        ...
        struct {
            asdl_seq *names;
        } Import;
    } v;
};
...
typedef struct _alias *alias_ty;
struct _alias {
    identifier name;
    identifier asname;
};
Include/asdl.h
typedef PyObject * identifier;

s->v.Import.namesが構造体alias_tyのリストになっており、alias_tyname及びasnameの組です。すなわち、import name1 as asname1, name2 as asname2, name3という文は

struct _alias { name: "name1", asname: "asname1" },
struct _alias { name: "name2", asname: "asname2" },
struct _alias { name: "name3", asname: NULL }

のような配列として表現されています。これらの個々の要素を順にasdl_seq_GET()を用いて取得します。
次に、ADDOP_LOAD_CONST()を用いて定数を2つ生成しています(これらはそれぞれIMPORT_NAME命令に渡されるlevel, fromlistを表しています)。またADDOP_NAME()を用いてIMPORT_NAME命令を生成します。さらに、name.を含んでいる場合、最初の部分のみを取り出します。これが、読み込んだモジュールを格納するための変数名となります。最後にcompiler_nameop()を呼び出し、変数を登録(普通はSTORE_GLOBAL命令の生成)しています。

IMPORT_NAME命令の実行

CPythonの内部バイトコードが生成されると、CPythonはこれを実行器Python/ceval.cを用いて実行します。

Python/eval.c
PyObject*
_PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
{
    ...
main_loop:
    for (;;) {
        ...
    dispatch_opcode:
        switch (opcode) {
            ...
        case TARGET(IMPORT_NAME): {
            PyObject *name = GETITEM(names, oparg);
            PyObject *fromlist = POP();
            PyObject *level = TOP();
            PyObject *res;
            res = import_name(tstate, f, name, fromlist, level);
            Py_DECREF(level);
            Py_DECREF(fromlist);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }
            ...
        }
        ...
    }
    ...
}
...
static PyObject *
import_name(PyThreadState *tstate, PyFrameObject *f,
            PyObject *name, PyObject *fromlist, PyObject *level)
{
    PyObject *import_func, *res;
    PyObject* stack[5];
    import_func = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___import__);
    ...
    Py_INCREF(import_func);
    stack[0] = name;
    stack[1] = f->f_globals;
    stack[2] = f->f_locals == NULL ? Py_None : f->f_locals;
    stack[3] = fromlist;
    stack[4] = level;
    res = _PyObject_FastCall(import_func, stack, 5);
    Py_DECREF(import_func);
    return res;
}

CPythonにおける実行器の実体は_PyEval_EvalFrameDefault()で、これは無限ループの中に巨大なswitch文が入っているような構造になっています。その中でIMPORT_NAME命令に関係する部分を抜き出しました。

まず、GETITEM()およびPOP()を用いて、構文解析で生成した引数name, fromlist, levelを取り出します。これらを用いてimport_name()を実行します。import_name()では、まず_PyDict_GetItemIdWithError()によってPythonで書かれた関数__import__()を取得しています。次に配列stack[]を用いて__import__()に渡す引数を用意し、_PyObject_FastCall()を用いて__import__()を実行します。

Pythonで書かれた部分: __import__()

import文を実行すると、結局はPythonで記述された__import__()が実行されることがわかりました。以下ではこの先の処理がどうなっているか見てみます。

注意

__import__()の実体はLib/importlib/_bootstrap.pyにありますが、実際にはこのファイルが直接実行されるわけではなく、実際にはCPythonのバイトコードに変換された_frozen_importlib/__import__()が実行されます。そのためLib/importlib/_bootstrap.pyやLib/importlib/_bootstrap_external.pyを変更した場合は、

$ make Programs/_freeze_importlib
$ ./Programs/_freeze_importlib importlib_bootstrap Lib/importlib/_bootstrap.py Python/importlib.h
$ ./Programs/_freeze_importlib importlib_bootstrap_external Lib/importlib/_bootstrap_external.py Python/importlib_external.h

等として、Python/importlib.h, Python/importlib_external.hを手動で再生成した上で、

$ make

を実行してpythonコマンドを再ビルドする必要があります。

__import__()関数のフォーマット

__import__()は以下のように定義されています。

Lib/importlib/_bootstrap.py
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
引数名 import文から呼ばれたときの値
name 読み込むモジュール名
globals 現在のフレームのグローバルオブジェクト(現在のモジュール)
locals 現在のフレームのローカルオブジェクト
fromlist None
level 0
  • name ... 読み込むモジュール名(例えばpub.sub..a.b.cなど)を指定します。例えばpub.subを与えた場合、__import__()はモジュールpubを読み込み、モジュールpub.subを読み込み、モジュールpub.subをモジュールpubsub属性として追加し、モジュールpubを返します。
  • globals ... 相対的なモジュール名を指定した場合の起点となるモジュール。
  • locals ... 無視されます。
  • fromlist ... from-import文で使われる、読み込むべき属性のリストです。
  • level ... globalsで指定したモジュールを起点として何個親に戻るか。

例えば、__main__モジュールからimport test.test2を実行したときは以下のように展開されます(localsは考えない)。

test = __import__("test.test2", sys.modules["__main__"], None, None, 0) 

__import__の処理の中身

Lib/importlib/_bootstrap.py
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
    package = globals['__name__']
    module = _gcd_import(name, package, level)
    if not fromlist:
        if level == 0:
            return _gcd_import(name.partition('.')[0])
        else:
            cut_off = len(name) - len(name.partition('.')[0])
            return sys.modules[module.__name__[:len(module.__name__)-cut_off]]
    else:
        ...

まず、相対的なモジュール名の起点となるパッケージの名前をpackageに取得します。そして_gcd_import()を用いてパッケージを読み込みます。
__import__()で重要な点は、読み込んだパッケージをそのまま返すわけではない、ということです。例えば、モジュールpub.sub__import__()で読み込む場合、パッケージpubとモジュールpub.subを両方読み込み、パッケージpubを表すPyObjectを返します。この処理をif not fromlist:の中で行っています。すなわち、level = 0の時、モジュールpub.subのロードは以下のように行われます。

_gcd_import("pub.sub", package, 0) # 戻り値はモジュール`pub.sub`
return _gcd_import("pub", package, 0) # 戻り値はパッケージ`pub`

1度の__import__()の呼び出しで_gcd_import()を2回実行しているため、非効率に思えます。実際、1回目の_gcd_import()の呼び出しで、パッケージpubとモジュールpub.subの読み込みが行われます。しかしながら一度読み込まれたモジュールはsys.modules[]にキャッシュされるため、2回目の呼び出しではこのキャッシュから取得されるだけです。

Lib/importlib/_bootstrap.py
def _gcd_import(name, package=None, level=0):
    ...
    if level > 0:
        name = _resolve_name(name, package, level)
    return _find_and_load(name)

def _resolve_name(name, package, level):
    bits = package.rsplit('.', level - 1)
    ...
    return '{}.{}'.format(bits[0], name)

def _find_and_load(name):
    ...
    module = sys.modules.get(name, _NEEDS_LOADING)
    if module is _NEEDS_LOADING:
        return _find_and_load_unlocked(name)
    ...
    return module

def _find_and_load_unlocked(name):
    path = None
    parent = name.rpartition('.')[0]
    if parent:
        if parent not in sys.modules:
            _gcd_import(parent)
        if name in sys.modules:
            return sys.modules[name]
        parent_module = sys.modules[parent]
        path = parent_module.__path__
    spec = _find_spec(name, path)
    module = _load_unlocked(spec)
    if parent:
        parent_module = sys.modules[parent]
        child = name.rpartition('.')[2]
        setattr(parent_module, child, module)
    return module

_gcd_import()ではまずresolve_path()を用いてname, package, levelから読み込むべきモジュール名nameを求めます。次に_find_and_load()が呼びだされ、求められたモジュールがすでにsys.modulesに存在しない場合は、_find_and_load_unlocked()が呼び出されます。

_find_and_load_unlocked()では、まず親パッケージが読み込まれたいない場合は_gcd_import()を再帰的に呼び出して読み込みます。そして_find_spec()及び_load_unlocked()を呼び出してモジュールを取得します。ここで、__path__はファイルシステム上のディレクトリのパスです(パッケージは必ず__path__を持つ)。最後に親パッケージにパッケージ名の属性を追加し、モジュールを返します。

Lib/importlib/_bootstrap.py
def _find_spec(name, path, target=None):
    ...
    for finder in sys.meta_path:
        spec = finder.find_spec(name, path, target)
        if spec is not None:
            ...
            return spec
    ...

_find_spec()では、sys.meta_pathを参照しています。後で述べるように、sys.meta_pathはfinderオブジェクトの配列です。_find_spec()では、sys.meta_pathの各要素に順にfind_spec()を実行し、specを取得します。specがNoneである場合は次のfinderを参照します。

Lib/importlib/_bootstrap.py
def _load_unlocked(spec):
    module = module_from_spec(spec)
    sys.modules[spec.name] = module
    if spec.loader is None:
        # A namespace package so do nothing.
    else:
        spec.loader.exec_module(module)
    return module

def module_from_spec(spec):
    module = None
    if hasattr(spec.loader, 'create_module'):
        module = spec.loader.create_module(spec)
    ...
    if module is None:
        module = _new_module(spec.name)
    _init_module_attrs(spec, module)
    return module

def _new_module(name):
    return type(sys)(name)

def _init_module_attrs(spec, module):
    if getattr(module, '__name__', None) is None:
        module.__name__ = spec.name
    if getattr(module, '__loader__', None) is None:
        loader = spec.loader
    if getattr(module, '__package__', None) is None:
        module.__package__ = spec.parent
    module.__spec__ = spec
    if getattr(module, '__path__', None) is None:
        if spec.submodule_search_locations is not None:
            module.__path__ = spec.submodule_search_locations
    if spec.has_location:
        if getattr(module, '__file__', None) is None:
            module.__file__ = spec.origin
        if getattr(module, '__cached__', None) is None:
            if spec.cached is not None:
                module.__cached__ = spec.cached
    return module

_load_unlocked()ではmodule_from_spec()を呼び出してモジュールを生成します。その後sys.modulesにモジュールを追加し、spec.loader.exec_module()を実行します。
module_from_spec()では、spec.loader.create_module()が存在する場合はそれを実行し、存在しない場合はspec.nameを名前に持つオブジェクトを作成しモジュールを得ます。その後_init_module_attrs()を実行してモジュールに必要な属性をセットします。

Finder, Spec, Loader

ここまでのまとめとして、Finder, Spec, Loaderに求められる要件を述べておきます。

Finder

sys.meta_pathに登録されるオブジェクト。Specを返す関数finder.find_spec()を持つ。

Spec

finder.find_spec()によって返されるオブジェクトであり、読み込むべきモジュールと対応している。通常はLoaderクラスのインスタンスである。

  • spec.name : 対応するモジュールの名前。
  • spec.submodule_search_locations : specがパッケージに対応する場合、パッケージの存在するディレクトリ。
  • spec.origin : 対応するモジュールのファイル名。
  • spec.parent : 親のパッケージ名。
  • spec.loader : 後述(名前空間パッケージではNone)

Loader

spec.loaderに格納されるオブジェクトであり、関数create_module()をもつ。

Finderの実装

前章で、配列sys.meta_pathが重要な役割を果たしていることがわかりました。
CPythonのREPL環境でsys.meta_pathを確認してみます。(通常のスクリプトでも同様の結果になります)

>>> import sys
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]

すなわち、sys.meta_path

  • BuiltinImporter
  • FrozenImporter
  • PathFinder

という3つのFinderから構成されていることがわかります。

BuiltinImporter

名前のとおり、組み込みパッケージを探すFinderです。

組み込みパッケージとは

組み込みパッケージはC言語で記述され、CPythonのバイナリに含まれています。

find_spec()

BuiltinImporterfind_spec()は以下のようになっています。

Lib/importlib/_bootstrap.py
class BuiltinImporter:
    @classmethod
    def find_spec(cls, fullname, path=None, target=None):
        if path is not None:
            return None
        if _imp.is_builtin(fullname):
            return ModuleSpec(fullname, cls, origin="built-in", is_package=False)
        else:
            return None

    @classmethod
    def create_module(self, spec):
        return _imp.create_builtin(spec)

    @classmethod
    def exec_module(self, module):
        _imp.exec_builtin(module)

class ModuleSpec:
    def __init__(self, name, loader, *, origin=None, loader_state=None,
                 is_package=None):
        self.name = name
        self.loader = loader
        self.origin = origin
        self.loader_state = loader_state
        self.submodule_search_locations = [] if is_package else None
        # file-location attributes
        self._set_fileattr = False
        self._cached = None

例えばspec = BuiltinImporter.find_spec("_imp")によって生成されるSpecオブジェクトは以下に示すものになります。

ModuleSpec{name: "_imp", loader: <BuiltinImporter>, origin: "built-in"}

BuiltinImporterfind_specによってつくられたspecに対してcreate_module()を実行すると、_imp.create_builtin()が実行されます。

Modules/config.c
struct _inittab _PyImport_Inittab[] = {
    // ... (外部モジュールの定義)
    {"marshal", PyMarshal_Init},
    {"_imp", PyInit__imp},
    {"_ast", PyInit__ast},
    {"builtins", NULL},
    {"sys", NULL},
    {"gc", PyInit_gc},
    {"_warnings", _PyWarnings_Init},
    {"_string", PyInit__string},
    {0, 0}
};
Python/import.c
static PyObject *
_imp_create_builtin(PyObject *module, PyObject *spec)
{
    PyThreadState *tstate = _PyThreadState_GET();
    struct _inittab *p;
    PyObject *name;
    const char *namestr;
    PyObject *mod;
    name = PyObject_GetAttrString(spec, "name");
    namestr = PyUnicode_AsUTF8(name);
    for (p = PyImport_Inittab; p->name != NULL; p++) {
        PyModuleDef *def;
        if (_PyUnicode_EqualToASCIIString(name, p->name)) {
            if (p->initfunc == NULL) {
                /* 'sys' 及び 'builtin'の場合はsys.modulesから取得して返す */
            }
            mod = (*p->initfunc)();
            // ...
            return mod;
        }
    }
}

PyMODINIT_FUNC
PyInit__imp(void)
{
    PyObject *m = PyModule_Create(&impmodule);
    PyObject *d = PyModule_GetDict(m);
    const wchar_t *mode = _PyInterpreterState_GET_UNSAFE()->config.check_hash_pycs_mode;
    PyObject *pyc_mode = PyUnicode_FromWideChar(mode, -1);
    PyDict_SetItemString(d, "check_hash_based_pycs", pyc_mode) < 0)
    Py_DECREF(pyc_mode);j
    return m;
}

static struct PyModuleDef impmodule = {
    PyModuleDef_HEAD_INIT,
    "_imp",
    "(Extremely) low-level import machinery bits as used by importlib and imp.",
    0,
    imp_methods,
    NULL,
    NULL,
    NULL,
    NULL
};

static PyMethodDef imp_methods[] = {
    {"extension_suffixes", (PyCFunction)_imp_extension_suffixes, METH_NOARGS, _imp_extension_suffixes__doc__},
    {"lock_held", (PyCFunction)_imp_lock_held, METH_NOARGS, _imp_lock_held__doc__},
    {"acquire_lock", (PyCFunction)_imp_acquire_lock, METH_NOARGS, _imp_acquire_lock__doc__},
    {"release_lock", (PyCFunction)_imp_release_lock, METH_NOARGS, _imp_release_lock__doc__},
    {"get_frozen_object", (PyCFunction)_imp_get_frozen_object, METH_O, _imp_get_frozen_object__doc__},
    {"is_frozen_package", (PyCFunction)_imp_is_frozen_package, METH_O, _imp_is_frozen_package__doc__},
    {"create_builtin", (PyCFunction)_imp_create_builtin, METH_O, _imp_create_builtin__doc__},
    {"init_frozen", (PyCFunction)_imp_init_frozen, METH_O, _imp_init_frozen__doc__},
    {"is_builtin", (PyCFunction)_imp_is_builtin, METH_O, _imp_is_builtin__doc__},
    {"is_frozen", (PyCFunction)_imp_is_frozen, METH_O, _imp_is_frozen__doc__},
    {"exec_builtin", (PyCFunction)_imp_exec_builtin, METH_O, _imp_exec_builtin__doc__},
    {"_fix_co_filename", (PyCFunction)(void(*)(void))_imp__fix_co_filename, METH_FASTCALL, _imp__fix_co_filename__doc__},
    {"source_hash", (PyCFunction)(void(*)(void))_imp_source_hash, METH_FASTCALL|METH_KEYWORDS, _imp_source_hash__doc__},
    {NULL, NULL}
};

_imp_create_builtin()ではPyImport_Inittab[]を参照し、読み込みたいモジュール名に対応するinitfunc()を実行します。例えば、_impモジュールに対応するinitfuncはPyInit__imp()で、これを実行するとimp_methods[]に含まれる関数を持つモジュールが作成されます。またcheck_hash_based_pycsという属性を追加しています。

FrozenImporter

名前のとおり、FrozenPackageを探すFinderです。

FrozenPackageとは

FrozenPackageは主にPythonで記述されていますが、高速化のためCPythonのバイトコードに変換された上でCPythonのバイナリにバンドルされています(変換はPrograms/freeze_importlib.cを用いて行われます)。以下にFrozenPackageの一覧を示します。(Python/frozen.cを参照)

FrozenPackage名 バイトコードを含むファイル 変換元のPythonスクリプト
_frozen_importlib Python/importlib.h Lib/importlib/_bootstrap.py
_frozen_importlib_external Python/importlib_external.h Lib/importlib/_bootstrap_external.py
zipimport Python/importlib_zipimport.h Lib/zipimport.py
__hello__ Python/frozen.c:M___hello__ Tools/freeze/flag.py (手動で変換)
__phello__ __hello__と同じ
__phello__.spam __hello__と同じ

実装

Lib/importlib/_bootstrap.py
class FrozenImporter:
    @classmethod
    def find_spec(cls, fullname, path=none, target=none):
        if _imp.is_frozen(fullname):
            is_package = _imp.is_frozen_package(fullname)
            return modulespec(fullname, cls, origin="frozen", is_package=is_package)
        else:
            return none

    @classmethod
    def create_module(cls, spec):
        """use default semantics for module creation."""

    @staticmethod
    def exec_module(module):
        name = module.__spec__.name
        code = _imp.get_frozen_object(name)
        exec(code, module.__dict__)

処理の中身はBuiltinImporterの場合と似ていますが、FrozenImporterではcreate_module()の実装を省略しています。これにより、空のモジュールが生成されます。その代わりに実装の実体はexec_module()内の_imp.get_frozen_object()及びexec()にあります。

Pythjon/import.c
static PyObject *
is_frozen_package(PyObject *name)
{
    const struct _frozen *p = find_frozen(name);
    if (p->size < 0)
        Py_RETURN_TRUE;
    else
        Py_RETURN_FALSE;
}

static const struct _frozen *
find_frozen(PyObject *name)
{
    const struct _frozen *p;
    for (p = PyImport_FrozenModules; ; p++) {
        if (p->name == NULL) return NULL;
        if (_PyUnicode_EqualToASCIIString(name, p->name))
            break;
    }
    return p;
}

static PyObject *
get_frozen_object(PyObject *name)
{
    const struct _frozen *p = find_frozen(name);
    int size = p->size;
    if (size < 0) size = -size;
    return PyMarshal_ReadObjectFromString((const char *)p->code, size);
}
Python/frozen.c
static const struct _frozen _PyImport_FrozenModules[] = {
    {"_frozen_importlib", _Py_M__importlib_bootstrap,
        (int)sizeof(_Py_M__importlib_bootstrap)},
    {"_frozen_importlib_external", _Py_M__importlib_bootstrap_external,
        (int)sizeof(_Py_M__importlib_bootstrap_external)},
    {"zipimport", _Py_M__zipimport,
        (int)sizeof(_Py_M__zipimport)},
    // ...
    {0, 0, 0} /* sentinel */
};
Python/importlib.h
const unsigned char _Py_M__importlib_bootstrap[] = {
    99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    0,4,0,0,0,64,0,0,0,115,194,1,0,0,100,0, // ...
};

このように、例えばfrozen_importlibの実体は配列_Py_M__importlib_bootstrap[]に格納されていることがわかります。

PathFinder

PathFinderの中身はシンプルです。

Lib/importlib/_bootstrap_external.py
class PathFinder:
    @classmethod
    def find_spec(cls, fullname, target=None):
        for entry in sys.path:
            if entry == '':
                entry = os.getcwd()
            try:
                finder = sys.path_importer_cache[entry]
            except KeyError:
                finder = None
                for hook in sys.path_hooks:
                    try:
                        finder = hook(entry)
                    except ImportError:
                        continue
                sys.path_importer_cache[entry] = finder
            if finder is not None:
                spec = finder.find_spec(fullname, target)
                if spec.loader is not None:
                    break
        else:
            # ... namespace packageのための処理
        return spec

PathFinderfind_spec()では直接specを生成することは行いません。代わりにsys.path[]に格納されたパスについて、sys.path_hooks[]を参照し、対応するFinderを探します。Finderが見つかったら、finder.find_spec()を呼び出します。

sys.path_hooks[]は対話環境から確認できます。

$ python
>>> import sys
>>> sys.path_hooks
[<class 'zipimport.zipimporter'>, <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x7f3806ece160>]

2つ目のpath_hook_for_FileFinder()FileFinderクラスのインスタンスを返す関数(クロージャ)で、FileFinderクラスのpath_hook()関数にあります。

Lib/importlib/_bootstrap_external.py
class FileFinder:
    # ...
    @classmethod
    def path_hook(cls, *loader_details):
        def path_hook_for_FileFinder(path):
            return cls(path, *loader_details)
        return path_hook_for_FileFinder

関数path_hook()の呼び出し部は以下のようになります。

Lib/importlib/_bootstrap_external.py
supported_loaders = [
    # ...
    (SourceFileLoader, ['.py']),
    (SourcelessFileLoader, ['.pyc'])
]
sys.path_hooks.extend([FileFinder.path_hook(*supported_loaders)])

この最後の行で、FileFinder.path_hook()の戻り値であるpath_hook_for_FileFinder()sys.path_hooks[]に追加されていることがわかります。よって、先程説明したPathFinder.find_spec()において各sys.path[]のパスを引数としてpath_hook_for_FileFinder()が実行され、その戻り値であるFileFinderクラスのインスタンスのfind_spec()が実行されます。

FileFinderの実装を見てみます。

Lib/importlib/_bootstrap_external.py
class FileFinder:
    def __init__(self, path, *loader_details):
        loaders = []
        for loader, suffixes in loader_details:
            loaders.extend((suffix, loader) for suffix in suffixes)
        self._loaders = loaders
        self.path = path or '.'

    def find_spec(self, fullname, target=None):
        cache_module = fullname.rpartition('.')[2]
        contents = _os.listdir(self.path)
        cache = set(contents)
        # モジュールがディレクトリ名である場合(パッケージ)
        if cache_module in cache:
            base_path = _path_join(self.path, cache_module)
            for suffix, loader_class in self._loaders:
                init_filename = '__init__' + suffix
                full_path = _path_join(base_path, init_filename)
                if _path_isfile(full_path):
                    loader = loader_class(fullname, full_path)
                    spec = _bootstrap.ModuleSpec(name, loader, origin=full_path)
                    spec._set_fileattr = True
                    spec.submodule_search_locations = submodule_search_locations
                    return spec
            else:
                # ... 名前空間パッケージのための処理
        # モジュール名が通常のファイルを指す場合
        for suffix, loader_class in self._loaders:
            full_path = _path_join(self.path, tail_module + suffix)
            if cache_module + suffix in cache:
                if _path_isfile(full_path):
                    loader = loader_class(fullname, full_path)
                    spec = _bootstrap.ModuleSpec(name, loader, origin=full_path)
                    spec._set_fileattr = True
                    spec.submodule_search_locations = None
                    return spec
        # 名前空間パッケージのための処理
        return None

FileFinder.find_spec()では、モジュール名がディレクトリ名やファイル名に一致しているかを調べ、一致している場合はModuleSpecクラスを用いてSpecを作成しています。このとき、spec.loaderを設定するためにself._loadersを参照しています。例えば、拡張子.pyの場合、SourceFileLoaderクラスのインスタンスがLoaderとして設定されることになります。

Spec取得後の処理

ここまでで、モジュールに対応するSpec及びLoaderが取得できました。ここで、先に示した_find_and_load_unlocked()の続きを見てみます。

Lib/importlib/_bootstrap.py
def _find_and_load_unlocked(name, import_):
    # ...
    spec = _find_spec(name, path)
    if spec is None:
        raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
    else:
        module = _load_unlocked(spec)
    # ...
    return module

def _load_unlocked(spec):
    module = module_from_spec(spec)
    # ...
    try:
        sys.modules[spec.name] = module
        try:
            # ...
            spec.loader.exec_module(module)
        except:
            try:
                del sys.modules[spec.name]
            except KeyError:
                pass
            raise
        # exec_module()内で別のモジュールを読み込んだ場合
        # sys.modules[] の最後尾に移動させる
        module = sys.modules.pop(spec.name)
        sys.modules[spec.name] = module
    # ...
    return module

def module_from_spec(spec):
    module = None
    if hasattr(spec.loader, 'create_module'):
        # If create_module() returns `None` then it means default
        # module creation should be used.
        module = spec.loader.create_module(spec)
    # ....
    if module is None:
        module = _new_module(spec.name)
    _init_module_attrs(spec, module)
    return module

def _new_module(name):
    return type(sys)(name)

このように、module_from_spec()内でspec.loader.create_module()という関数を呼び出してモジュールを生成しています。
生成したモジュールは、sys.modules[]の最後尾に追加されます。
その後、spec.loader.exec_module(module)が呼び出されます。

Loader内の処理

それでは、SourceFileLoaderクラスの実装を見てみます。

Lib/importlib/_bootstrap_external.py
class SourceFileLoader:
    def create_module(self, spec):
        return None

    def exec_module(self, module):
        source_path = self.path
        with _io.open_code(source_path) as file:
            source_bytes = file.read()
        code_object = compile(source_bytes, source_path, "exec", dont_inherit=True, optimize=-1)
        exec(code_object, module.__dict__)

ここで、create_module()Noneを返します。よって、デフォルトの挙動である_new_module()が実行され、module = type(sys)(name)によってモジュールが生成されます。
その後、exec_module()の呼び出しでは、以下のような処理が行われています。

  • ソースコードを読み込む
  • compile()を用いてcode_objectを生成する
  • exec()及びmoduleを用いてモジュールを実行する

まとめ

まとめると、通常のテキスト形式のソースファイルに対応するモジュールの読み込み時には以下のような処理が行われていることになります。

  • 辞書sys.modulesに存在する場合、それを使用する。
  • sys.modulesに存在しない場合、sys.meta_pathを参照する。
  • sys.meta_pathにはFinderオブジェクトが格納されている。各Finderオブジェクトに対し、finder.find_spec()を実行する。
  • 通常のモジュールでは、PathFinderが用いられる。
  • PathFinder.find_spec()は、sys.pathを参照して、各パスに対応するFileFinderを生成し(このときsys.path_hooks[]に含まれるFinder生成関数が順に実行される)、FileFinder.find_spec()sys.pathの順番で実行する。
  • FileFinder.find_spec()では、self.pathに対応するディレクトリ内のファイルを検索し、ファイルが見つかった場合ModuleSpec, 見つからなかった場合Noneを返す。その際、ModuleSpec.loaderとしてSourceFileLoaderが渡される。
  • SourceFileLoader.create_module() でモジュールが生成されるが、これは実際のところ何もしないので、代わりにtype(sys)(name)を用いてモジュールが生成される。
  • 生成されたモジュールがsys.modulesにキャッシュされる。
  • SourceFileLoader.exec_module()によってモジュールが実行される。この内部では、ファイルを読み込み、バイトコードにコンパイルし、exec()を用いて実行している。

おわりに

これで、Pythonにが苦手な人がまた一人増えたのではないでしょうか。


  1. ディレクトリmy_moduleのみがあり、ファイルmy_module/__init__.pyが存在しない事もあります。その場合は名前空間パッケージと呼ばれます。 

  2. 名前空間パッケージの場合は、ファイルの実体がない(ディレクトリのみ)ため使用できません。代わりに__path__でディレクトリのリストを取得できます。 

  3. 厳密な定義ではない。 

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

RubyとPythonの言語の超簡易的比較

RubyとPythonの比較

Ruby(Rails)の学習も一通り終わり、次の学習言語としてPythonを学んでいこうと思う.
ただ、Pythonとは何か?
どちらも可読性が高いのは知っているが、具体的な違いを持って調べてみた。

Ruby

1.Rubyは日本人が開発した言語で日本語の文献が多いので比較的、日本人が学習しやすい。
2.Webアプリケーション開発で主に使用される言語(例:twitter)
3.Ruby on RailsなどのすぐにWebアプリケーション(サイト等)を作成するためのフレームワークがあるのでベンチャー企業などに使われることが多い。

Python

1.シンプルで覚えることが少ない構文。他の言語と比べて短く簡単にコードが書ける(Rubyとどのぐらい差があるかは学習して感じていく)
2.Webアプリケーションにも扱われることもあるが、真の実力を発揮する分野としては機械学習とAI開発にある
3.例として機械学習では大量のデータ(ビックデータ)から解析や計算をすることができるので、マーケティングに使われることもある。(YouTube)

Pythonの学習の進め方(お金を掛けたくない自己流独学の予定)

1.Progateで基礎中の基礎を学ぶ
2.paizaで動画の閲覧(ドットインストールでも可)
3.図書館から書籍を借り、コードを記述しながら学習(どういうアプリを作成するか考えながら)
4.アイディアを浮かべたものをオリジナルアプリとして形にする(AWSアカウントは別のものでもう使っているのでherokuへデプロイ予定)

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

機械学習の評価指標

はじめに

研究で医療データを扱うことが多く、よく使用する評価指標をまとめました。簡単なものばかりですが、自分のメモとして残しておきますので少しでも参考になればと思います。知っている人は特に読む必要は無いようなことばかりですのでこの記事はスルーしてください。

混同行列 (Confusion Matrix)

簡単に言うと、テストデータに対する予測を表にまとめたものです。
まず、「病気である」と判断されたものをPositive (陽性)、「病気でない」と判断されたものをNegative (陰性) とします。一般的に疾患がある方を1(Positive)とする傾向があります。

- 検査(陽) 検査(陰)
診断(陽) TP FN
診断(陰) FP TN

Confusion Matrix は大きく以下の4グループに分けることができます。

  • TP(True Positive :真陽性) : 検査も診断も陽性と判定した個数

  • TN(True Negative :真陰性) : 検査も診断も陰性と判定した個数

  • FN(False Negative :偽陰性) : 検査は陰性と判定したが診断は陽性と判定されたした個数

  • FP(False Positive :偽陽性) : 検査は陽性と判定したが診断は陰性と判定された個数

また、Confusion Matrix はsklearnで簡単で作成できます。

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_true, y_pred)
print(cm)

*y_true:正解ラベル, y_pred:テストの予測クラス
*この時、各軸は各クラスの値をソートした順番になります。順番には十分気をつけて下さい。

- 検査(N) 検査(P)
診断(N) TN FP
診断(P) FN TP

順番を変更したい際は、引数にlabelsで指定して下さい。

confusion_matrix(y_true, y_pred, labels=['1', '0'])
- 検査(P) 検査(N)
診断(P) TP FN
診断(N) FP TN

TP, TN, FP, FN の個数を取得する方法

cm = confusion_matrix(y_true, y_pred)
tn, fp, fn, tp = cm.flatten()

*ただしこの場合は、0 or 1 分類で、0を陰性としている場合である。

評価指標

  • Accuracy (正確度)
Accuracy = \frac{TP + TN}{TP + FN + TN + FP}
  • Sensitivity (感度)

病気の人を正しく病気と判断できた割合。言い換えると、病気の人を見逃さなかった割合。再現率(recall)とも言う。感度が高い検査は、検診など病気の有無を調べる際に有効(除外診断)。

Sensitivity = \frac{TP}{TP + FN}
  • Specificity (特異度)

病気でない人を病気でないと正しく判断できた割合。特異度が高い検査は、病気を確定する検査で有効(確定診断)。

Specificity = \frac{TN}{TN + FP}
  • PPV(Positive Predictive Value :陽性的中率)

検査で陽性と判断されている中で、実際に病気の人の割合。

PPV = \frac{TP}{TP + FP}
  • NPV(Negative Predictive Value :陰性的中率)

検査で陰性と判断されている中で、実際に陰性の人の割合。

NPV = \frac{TN}{TN + FN}

感度を高くしようとすると特異度が低くなり、特異度を高くしようとすると感度が低くなるトレードオフの関係にあります。目的に合わせて閾値を設定することが望まれます。

ROC曲線

ROC曲線(Receiver Operating Characteristic curve)は、診断法がどのくらい有用なのか確認するのに使用されます。機械学習であれば、どのくらいモデルが有用であるか確認できます。縦軸に感度、横軸に(1-特異度)をとります。曲線がグラフ左上の角に近い位置にあるほど感度と特異度が優れており、この値を最適なカットオフ値としています。

sklearnを用いると簡単にROC曲線は描くことができます。

import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(t_label, p_score)     
plt.plot(fpr, tpr, marker='o')
plt.xlabel('FPR')
plt.ylabel('TPR')
plt.grid()
plt.savefig('plots/roc_curve.png')

*t_label : 正解ラベル, p_score:予測確率

AUC

AUC(Area Under Curve)とは、ROC曲線の下の面積で0~1をとる。ランダムの時は、0.5になります。値が1に近いほど性能が良いことになります。

AUCは以下のように求めることができます。

from sklearn.metrics import roc_auc_score
print(roc_auc_score(y_true, y_score))

終わりに

簡単にですがメモ程度でまとめてみました。
今更感がありますが、しばらく触れていないと忘れるので記事として残しておきます。

参考文献

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

何百~何千通りも答えのある箱詰めパズルを解く(レベル2:ペントミノ)

前回は「何百~何千通りも答えのある箱詰めパズルを解く」ということで、レベル1のテトロミノを解くプログラムをPythonで実装しましたが、レベル2のペントミノを解くことはできませんでした。今回は、改めてペントミノに挑戦します!

私の自宅にあるのは、こういうやつです。

前回の復習も兼ねて

xy のケースを初期化するコードは次のようになります。前回は -1 で初期化しましたが、今回は事情があって(後ほど説明します)、 0 で初期化するように変更しました。

def generate(x, y): # 0 で初期化するよう変更
    return [[0 for _ in range(x)] for _ in range(y)]

同様に、新しいピースを得る次の関数でも、 「ピースで埋められていないマス」を意味する -1 の部分を 0 に置き換えました。

def get_new_piece(matrix, piece_size):
    piece = []

    for x in range(x_length):
        for y in range(y_length):
            if matrix[y][x] == 0: ### 変更箇所
                piece.append([x, y])
                break
        if len(piece) > 0:
            break

    for i in range(piece_size - 1):
        neighbors = get_rand_neighbor(piece, matrix)
        random.shuffle(neighbors)
        for x, y in neighbors:
            if [x, y] not in piece:
                piece.append([x, y])
                break

    return piece

同様に、隣接するマスを得る次の関数でも、 「ピースで埋められていないマス」を意味する -1 の部分を 0 に置き換えました。

def get_rand_neighbor(piece, matrix):
    neighbors = []
    for x, y in piece:
        for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
            if x - dx < 0 or x - dx >= len(matrix[0]):
                pass
            elif y - dy < 0 or y - dy >= len(matrix):
                pass
            elif matrix[y - dy][x - dx] == 0: ### 変更箇所
                neighbors.append([x - dx, y - dy])

    return neighbors

パズルを図示する関数は、そのままです。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def depict(matrix):
    plt.imshow(matrix, interpolation='nearest', cmap=plt.cm.gist_ncar)
    plt.xticks(np.arange(len(matrix[0])), range(len(matrix[0])), rotation=90)
    plt.yticks(np.arange(len(matrix)), range(len(matrix)))
    plt.tight_layout()
    plt.show()

同一構造を判定する関数の改良

今回の重要な変更点は、ピースの同一構造を判定する次の関数です。

テトロミノは解けるのにペントミノはどうして解けないのかな〜〜〜??

と思っていろいろ試行錯誤していましたが、改良前のこの関数ではテトロミノのピースが全て区別できるのに対し、ペントミノのピースの中に区別できないものがあるということが分かりました。

そりゃペントミノ解けんわな...

ということで、改良済みの関数はこちらです。

def shape_key(piece):
    distances = []
    adjacents = {} ### 今回新たに加えた特徴量
    for i in range(len(piece)):
        xy1 = piece[i]
        for j in range(len(piece)):
            if i < j:
                xy2 = piece[j]
                distance = (xy1[0] - xy2[0])**2 + (xy1[1] - xy2[1])**2
                distances.append(distance)

                ### 以下、今回新たに加えた特徴量
                if distance == 1:
                    if i not in adjacents.keys():
                        adjacents[i] = []
                    adjacents[i].append(j)
                    if j not in adjacents.keys():
                        adjacents[j] = []
                    adjacents[j].append(i)

    #return "".join(str(sorted(distances))) 前回の返り値

    ### 今回の返り値
    return (
        "".join(str(sorted(distances))) + "_" 
        + "".join(str(sorted([len(adjacents[k]) for k in adjacents.keys()]))))

テトロミノ(レベル1)を解きます

ペントミノ(レベル2)を解く前に、テトロミノ(レベル1)が解けることを確認します。変更箇所がもう1つあることに注意。ランダムに試行錯誤を繰り返すので、実行するたびに出力結果は変わります。

import random
x_length = 8
y_length = 5
piece_size = 4
same_piece_limit = 2
max_trial = 530000
best_score = x_length * y_length
best_matrix = generate(x_length, y_length)

for trial in range(max_trial):
    matrix = generate(x_length, y_length)
    count = {}
    piece_id = 1 ### 変更箇所
    piece = get_new_piece(matrix, piece_size)
    key = shape_key(piece)
    count[key] = 1

    while len(piece) == piece_size:
        for x, y in piece:
            matrix[y][x] = piece_id

        piece = get_new_piece(matrix, piece_size)
        key = shape_key(piece)
        if key not in count.keys():
            count[key] = 0
        count[key] += 1
        if count[key] > same_piece_limit:
            break

        piece_id += 1

    score = sum([sum([1 if n == 0 else 0 for n in mat]) for mat in matrix])

    if best_score >= score:
        best_score = score
        best_matrix = matrix
        #depict(best_matrix)

    if score == 0:
        print(trial, "th trial")
        break

depict(best_matrix)
best_matrix
71 th trial

noblock_puzzle4_11_1.png

[[1, 1, 1, 1, 7, 7, 7, 7],
 [2, 2, 4, 6, 6, 6, 9, 9],
 [2, 4, 4, 4, 8, 6, 9, 9],
 [2, 3, 3, 5, 8, 8, 10, 10],
 [3, 3, 5, 5, 5, 8, 10, 10]]

ペントミノ(レベル2)を解きます

変数の値を変更すれば、ペントミノ(レベル2)を解けるはずです。やってみましょう。

x_length = 10
y_length = 6
piece_size = 5
same_piece_limit = 1
max_trial = 530000
best_score = x_length * y_length
best_matrix = generate(x_length, y_length)

for trial in range(max_trial):
    matrix = generate(x_length, y_length)
    count = {}
    piece_id = 1 ###
    piece = get_new_piece(matrix, piece_size)
    key = shape_key(piece)
    count[key] = 1

    while len(piece) == piece_size:
        for x, y in piece:
            matrix[y][x] = piece_id

        piece = get_new_piece(matrix, piece_size)
        key = shape_key(piece)
        if key not in count.keys():
            count[key] = 0
        count[key] += 1
        if count[key] > same_piece_limit:
            break

        piece_id += 1

    score = sum([sum([1 if n == 0 else 0 for n in mat]) for mat in matrix])

    if best_score >= score:
        best_score = score
        best_matrix = matrix
        #depict(best_matrix)

    if score == 0:
        print(trial, "th trial")
        break

depict(best_matrix)
best_matrix

noblock_puzzle4_13_0.png

[[1, 1, 1, 5, 6, 6, 6, 6, 10, 0],
 [1, 1, 5, 5, 7, 7, 6, 10, 10, 11],
 [2, 2, 4, 5, 5, 7, 10, 10, 11, 11],
 [2, 4, 4, 4, 8, 7, 7, 9, 11, 0],
 [2, 2, 4, 3, 8, 9, 9, 9, 11, 0],
 [3, 3, 3, 3, 8, 8, 8, 9, 0, 0]]

うーむ、、、ペントミノ(レベル2)のピースは全て区別できるようになったはずだけど、530000回繰り返してもまだ解けないとは...。ほっほっほ、初めてですよ...ここまで私をコケにしたおバカさん達は…

探索方法を変更

上記の方法では、ランダムな探索を繰り返していました。このため、同じ場所の探索を何度も繰り返している可能性があります。

そこで、次のような改良を加えることで、いくつか無駄な探索を減らす努力をしてみました。

  • グラフ理論でいう「順位優先探索」を行なうことで、同じ場所の探索を二度と繰り返さないようにした。

  • グラフ理論でいう「連結成分」という概念を用いて、ピースに埋められていないマスの連結成分が2つ以上になるピースの置き方をした時は、それ以上の探索を打ち切ることにした。

  • これまでに置いたピースの集合と、ピースを置いていないマスの境界線を「表面」とし、新しく置くピースがその表面と大きく接するとき、そのピースを優先するようにした。

  • 全ピースの半分だけケースに収める計算ならすぐできるので、半分だけ収める計算を行い、その「半分だけ解いたパズル」の組み合わせの中から条件を満たす組み合わせを発見する方針に変更した。

次に置けるピースを全列挙する

次に置けるピースを全列挙する関数がこちらになります。これを用いて順位優先探索することになります。

import copy

def get_new_pieces(matrix):
    piece = []

    for x in range(x_length):
        for y in range(y_length):
            if matrix[y][x] == 0:
                piece.append([x, y])
                break
        if len(piece) > 0:
            break

    result_pieces = []
    waiting = []
    waiting.append(piece)
    while len(waiting) > 0:
        piece = waiting.pop()
        neighbors = get_rand_neighbor(piece, matrix)
        for x, y in neighbors:
            if [x, y] not in piece:
                new_piece = copy.deepcopy(piece)
                new_piece.append([x, y])
                if len(new_piece) == piece_size:
                    new_piece = sorted(new_piece)
                    if new_piece not in result_pieces:
                        result_pieces.append(new_piece)
                else:
                    waiting.append(new_piece)

    return sorted(result_pieces)

ピースを置いてない場所の連結成分

ピースに埋められていないマスの連結成分を得る関数はこちらです。

def get_connected_subgraphs(matrix):
    neigh = {}
    for y in range(len(matrix)):
        for x in range(len(matrix[y])):
            if matrix[y][x] == 0:
                neigh[",".join([str(x), str(y)])] = []
                for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
                    if x - dx < 0 or x - dx >= len(matrix[0]):
                        pass
                    elif y - dy < 0 or y - dy >= len(matrix):
                        pass
                    elif matrix[y - dy][x - dx] == 0:
                        neigh[",".join([str(x), str(y)])].append([x - dx, y - dy])

    connected_subgraphs = []
    found = []
    for k, v in neigh.items():
        if k in found:
            continue
        connected_subgraph = [k]
        waiting = list(v)
        found.append(k)
        while len(waiting):
            x, y = waiting.pop()
            n = ",".join([str(x), str(y)])
            if n in found:
                continue
            connected_subgraph.append(n)
            found.append(n)
            if n in neigh.keys():
                waiting += neigh[n]
        connected_subgraphs.append(connected_subgraph)

    return connected_subgraphs

「表面」の大きさを決める関数

新しいピースを置いた時、既存のピースの集合と接する「表面」の大きさを interface とし、これを順位優先探索に用いることにしました。(surface も計算しますが、これは結局使いません)

def get_face(piece, matrix): 
    interface = 0
    surface = 0
    for x, y in piece:
        for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
            if x - dx < 0 or x - dx >= len(matrix[0]):
                pass
            elif y - dy < 0 or y - dy >= len(matrix):
                pass
            elif matrix[y - dy][x - dx] == 0:
                surface += 1
            else:
                interface += 1
    return interface, surface

半分だけパズルを解く

今回の探索方針変更の大きなところです。全ピースを最後までケースに置こうとする方針では、テトロミノ(レベル1)は解けますがペントミノ(レベル2)は解けませんでした。そこで、最後まで埋めようとせず、全ピースの半分だけケースに置くことを繰り返し、その「半分だけ解いたパズル」を組み合わせて、いい組み合わせを探すことをします。

そのうち「半分だけ解いたパズル」を作る関数がこちらです。

また、基本的には「順位優先探索」ですが、ある低い確率で順位をランダムにする処理を加えています。

import random
def half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    best_score = x_length * y_length
    best_matrix = generate(x_length, y_length)
    n_depict = 0
    n_pieces = int(x_length * y_length / piece_size)
    waiting = []
    piece_id = 1
    matrix = generate(x_length, y_length)
    for new_piece in get_new_pieces(matrix):
        pieces2count = {}
        key = shape_key(new_piece)
        pieces2count[key] = 1
        new_matrix = copy.deepcopy(matrix)
        for x, y in new_piece:
            new_matrix[y][x] = piece_id
        pieces = [new_piece]
        waiting.append([0, piece_id + 1, pieces, new_matrix, pieces2count])

    trial = 0
    random.shuffle(waiting)
    while len(waiting) > 0:
        trial += 1
        if trial > 530000:
            break

        delta, piece_id, pieces, matrix, pieces2count = waiting.pop()
        score = sum([sum([1 if x == 0 else 0 for x in mat]) for mat in matrix])
        if len(get_connected_subgraphs(matrix)) > 1:
            continue

        if best_score >= score:
            best_score = score
            best_matrix = matrix

        if score == (x_length * y_length) / 2:
            yield(best_matrix)
            continue

        new_pieces = get_new_pieces(matrix)
        for new_piece in new_pieces:
            new_pieces2count = copy.deepcopy(pieces2count)
            key = shape_key(new_piece)
            if key not in new_pieces2count.keys():
                new_pieces2count[key] = 0
            new_pieces2count[key] += 1
            if new_pieces2count[key] > same_piece_limit:
                continue

            new_pieces = copy.deepcopy(pieces)
            new_pieces.append(new_piece)
            new_matrix = copy.deepcopy(matrix)
            for x, y in new_piece:
                new_matrix[y][x] = piece_id

            face = get_face(new_piece, matrix)

            if len(get_connected_subgraphs(matrix)) > 1:
                continue
            waiting.append([face[0], piece_id + 1, new_pieces, new_matrix, new_pieces2count])

        if random.random() < 0.05:
            random.shuffle(waiting)
        elif random.random() < 0.95:
            waiting = sorted(waiting)            
    return matrix

同じ形のピースが指定個数以下かどうか確認する

同じ形のピースが指定個数以下かどうか確認する関数です。同じ目的のために作ったディクショナリーとして pieces2count を前回使ったので、無駄といえば無駄かもしれません。改良の余地があるところです。

今回この関数を作った理由は、「半分だけ収める計算を行い、その組み合わせの中から条件を満たす組み合わせを発見する方針」に変更したため、半分解いた答えを2つガッチンコした combined_matrix に対して同じ形のピースが指定個数以下かどうか確認したかったからです。

def same_piece_within_limit(matrix, same_piece_limit):
    id2piece = {}
    for y in range(len(matrix)):
        for x in range(len(matrix[y])):
            if matrix[y][x] not in id2piece.keys():
                id2piece[matrix[y][x]] = []
            id2piece[matrix[y][x]].append([x, y])

    key2count = {}
    for id, piece in id2piece.items():
        key = shape_key(piece)
        if key not in key2count.keys():
            key2count[key] = 0
        key2count[key] += 1
        if key2count[key] > same_piece_limit:
            return False

    return True

テトロミノ(レベル1)を解きます

以上の関数を用意したら、テトロミノ(レベル1)が解けるか確認です。

-1 ではなく 0 で初期化するように変更しましたが、それは half_puzzle から出てきた「半分だけ解いたパズル」を組み合わせたとき、ぴったりハマったかどうか判定するときに生きてきます。

実行するたびに違う答えが出てくるはずです。

x_length, y_length, piece_size, same_piece_limit = 8, 5, 4, 2

index = 0
matrix_history = []
keta = int(x_length * y_length / piece_size)
for matrix in half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    for prev_matrix in matrix_history:
        matrix3 = np.flipud(np.fliplr(np.array(matrix)))

        if (prev_matrix + matrix3).min().min() > 0:
            matrix3 += keta
            matrix3 = np.where(matrix3 == keta, 0, matrix3)
            combined_matrix = prev_matrix + matrix3
            if same_piece_within_limit(combined_matrix, same_piece_limit):
                depict(combined_matrix)
                index += 1

    matrix_history.append(matrix)
    if index > 5:
        break

noblock_puzzle4_25_0.png

noblock_puzzle4_25_1.png

noblock_puzzle4_25_2.png

noblock_puzzle4_25_3.png

noblock_puzzle4_25_4.png

noblock_puzzle4_25_5.png

noblock_puzzle4_25_6.png

ペントミノ(レベル2)を解きます。

いよいよ、ペントミノ(レベル2)に挑戦です!

様々な計算効率化を実装したので、今度こそ!

x_length, y_length, piece_size, same_piece_limit = 10, 6, 5, 1

index = 0
matrix_history = []
keta = int(x_length * y_length / piece_size)
for matrix in half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    for prev_matrix in matrix_history:
        matrix3 = np.flipud(np.fliplr(np.array(matrix)))

        if (prev_matrix + matrix3).min().min() > 0:
            matrix3 += keta
            matrix3 = np.where(matrix3 == keta, 0, matrix3)
            combined_matrix = prev_matrix + matrix3
            if same_piece_within_limit(combined_matrix, same_piece_limit):
                depict(combined_matrix)
                index += 1

    matrix_history.append(matrix)
    if index > 5:
        break

noblock_puzzle4_27_0.png

noblock_puzzle4_27_1.png

noblock_puzzle4_27_2.png

noblock_puzzle4_27_3.png

noblock_puzzle4_27_4.png

noblock_puzzle4_27_5.png

解けた〜〜!!!

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

Azure で無料・お手軽にドキュメントページをデプロイする(MkDocs + Static Web Apps)

この記事は Advent Calendar 2020 「求ム!Cloud Nativeアプリケーション開発のTips!【PR】日本マイクロソフト」 の1日目の記事です。

Python 製の静的サイトジェネレーターを Azure Static Web Apps にデプロイしてみたよという話をします。

他にも、静的サイトって何?みたいな話やどんな Python フレームワークで作成したアプリが Azure Static Web Apps にデプロイできるのかという話もします。

スクリーンショット 2020-12-01 1.32.23.png

? モチベーション

開発をしていると ? ドキュメントサイト を公開したくなることがあると思います。ですが,

  • ?‍? フロントエンドの技術スタックがない
  • ✨ 簡単・いい感じにウェブページを作りたい
  • ⛏ メンテしたくない、運用コストかけたくない

みたいなお気持ちになったことはないでしょうか??私はあります。

この記事では,↑のような方にとって

  • Python 製静的サイトジェネレータ MkDocs で作ったドキュメントサイトを
  • Azure Static Web Apps にデプロイする

のが一つの解かも知れんよ,という話をしたいと思います。普段フロントエンドに携わっていない人でも「Web アプリ作ってデプロイするの簡単じゃん」と思っていただけたら嬉しいです。

スクリーンショット 2020-12-01 1.42.53.png

?‍?‍? 対象読者と前提

この記事は,以下の人にむけた内容になっています

  • Azure を使っていてドキュメントサイト作りたいな〜という人
  • フロントエンドよくわからないけど,
    • いい感じのデザインにしたい!な人
    • 低コストで簡単にウェブページをデプロイ・管理したい人
  • Web アプリも Python で作りてえんだ!な人

また,以下の内容については既知 or 既にあるという前提で話を進めます

  • Python 3.x がインストールしてある
  • Git 使ったことある,GitHub のアカウントがある
  • VSCode 使ったことある
  • Azure のアカウントがある(今回使う Azure Static Web Apps は無料で利用できます)

? どうやってやるの?

大まかな流れは以下のようになります:

1. 静的サイトジェネレータのセットアップ
2. リポジトリのセットアップ
3. Azure Static Web Apps のリソース作成
4. CI/CD のセットアップ

次に,使用する 静的サイトジェネレーターAzure Static Web Apps について説明します

静的サイトジェネレーター - MkDocs

静的サイトジェネレータ 1として,本記事では Python 製の OSS である MkDocs を使います。

特にフロントエンド開発未経験の方がドキュメントサイトを構築するのに,MkDocs を使うのが良い選択肢であると感じる理由は以下の通りです。

  • 機能面
    • プラグインが充実 (数式表現,コードのシンタックスハイライトなど)
    • デフォルトで全文検索機能をサポート
    • テーマが充実 (pip install で利用可能)
  • 手軽さ
    • 設定ファイルで簡単にカスタマイズ可能(HTML,CSS,JS が隠蔽)
    • 環境構築の簡単さ
      • pip install mkdocs && mkdocs new mysite && cd mysite && mkdocs serve

世の中にはいろんな静的サイトジェネレータがあり,その中でもドキュメントサイトに特化した OSS をいくつか使ったことがあるのですが,MkDocs は必要十分な機能を簡単にプラグインとして取り込めるのが良いところです。

? NOTE
機能を網羅的に知りたい方は,@mebiusbox2 さんの『MkDocsによるドキュメント作成』 が参考になります。

ちなみに,Python の有名なフロントエンドフレームワーク django や Flask と MkDocs はどのように違うのでしょうか?前者は基本的にサーバー側で動的に HTML を生成するので,Python 実行環境が必要になります。しかし,MkDocs の場合は HTML を生成するのはビルド時のみなので,あとはビルド時に生成した静的コンテンツをホスティングするだけです。

図にすると以下のようになります。
スクリーンショット 2020-12-01 1.32.23.png

動的に HTML を生成する django や Flask はサービス提供時も常に Python 実行環境が稼働している必要があるので,Web Apps や VM といったリソースを使う必要があります。しかし,MkDocs では, CI/CD などでビルドして一度 HTML を生成したら,それをホスティングするサービスさえあれば十分です。そこで,デプロイ先として Azure Static Web Apps や Azure Storage Blob などの,ホスティングに最適化したサービスを選べばコストも抑えられますし,ホスティングサーバの面倒は Azure がみてくれるので(サーバーレス)便利です。

Azure Static Web Apps

ちょうど最近リリースされたばかりの Azure のリソースで,現在はプレビュー版となっていますが,Azure を使っているなら 今後フロントエンド開発における最良のリソースの選択肢の1つになると注目されている 2 ものです (公式 docs はこちら)。

理由は,以下の通りです。

  • 無料 3
  • 高速 4
  • 便利機能の数々!(認証,ブランチごとの自動デプロイ...)
  • サーバーレス

無料で使えるホスティングサービスには例えば Netlify や GitHub Pages などがありますが,Azure が優れているのは 地理的分散 による高速化のための仕組みや 認証機能ブランチごとの自動デプロイ機能 などがデフォルトでサポートされている点です。またサーバーレスであり,(定義上)リソースのメンテナンスは不要 5です。

Azure Static Web Apps についてのより詳しい話は miyake さんの 『Azure App Service に Static Web Apps が登場!』 をご覧ください。

? NOTE

現在 Azure Static Web Apps は preview 版 であり, GitHub + GitHub Actions 以外のコードベースと CI/CD サービスをサポートしていません6 。以下に該当する方は,Azure Storage Blob を用いる方法 7 をオススメします。

  • production 環境で使用することを検討している場合
  • GitHub を利用していない場合(GitLab, Azure DevOps など)

Azure Storage Blob を使ったデプロイの方法は,この記事の後半の方に簡単に書いています。

使用する技術スタック

以上をまとめると,この記事で使用する技術スタックは以下の感じになります。

名前 用途
GitHub Repos 記事・設定・パイプラインの管理
GitHub Actions ビルド・デプロイ用パイプライン
MkDocs 静的サイトジェネレータ。アプリのソースコード(HTML,CSS,JS)を生成する
Azure Static Web Apps 静的サイトのホスティング

それでは早速,やり方に入っていきます。

? 具体的な方法

MkDocs のローカルセットアップ

ローカル環境で mkdocs のセットアップを行います。この手順を省略したい場合は このリポジトリを clone してください。

1. インストール

pip install mkdocs                # mkdocs のインストール
pip install mkdocs-material       # mkdocs のテーマのインストール
mkdocs new my-docs && cd my-docs  # mkdocs プロジェクトの作成
mkdocs serve                      # 開発サーバーを立ち上げる

↑ テーマのインストールは任意ですが,ここでは1番人気のある mkdocs-material をインストールします。他のテーマは こちら から見ることができます。

2. 動作確認

プロジェクトのルートディレクトリで mkdocs serve を実行して,開発サーバを立ち上げたら ブラウザから http://localhost:8000 にアクセスしてみましょう。

ちなみに,プロダクション用のビルドは mkdocs build --clean で実行できます。これを実行すると,/site ディレクトリが生成され,その中に静的なコンテンツ (HTML, CSS, JS...) が生成されます。

以降の手順では,このプロジェクトを Git で管理し GitHub 上のリモートリポジトリに push していきます。説明されなくても分かるよ!と言う人は飛ばしてください。

3. Git リポジトリとして管理する

動作確認ができたら,このプロジェクトを Git で管理します。

git init

4. .gitignore を作成

mkdocs では,コンテンツ(markdown ファイル)や設定ファイルをビルドして HTML,CSS,JS のファイルを生成し,これらを site ディレクトリに格納します。ただし,これらは Git で管理したいものではないので,無視するために .gitignore ファイルを生成します。

   echo 'site' > .gitignore

5. コミットする

git add .
git commit -m "init mkdocs project"

GitHub のセットアップ

  1. GitHub で空のリポジトリを作成します。

  2. 作成したリモートリポジトリに,先ほど作成したローカルリポジトリの内容を push します。

git remote add origin https://github.com/${user_id}/${repos_name}.git
git branch -M main
git push -u origin main

${user_id}${repos_name} にはそれぞれあなたの GitHub ユーザー ID と作成したリポジトリ名を指定します。

Azure Static Web Apps の作成

次は,さっき作った mkdocs の静的コンテンツをデプロイする Azure Static Web Apps のリソースを作成します。

1. VSCode の Azure Static Web Apps 拡張機能のインストール

VSCode に下記の拡張機能を追加します。これにより,リポジトリと Azure リソースの統合がシームレスになります。
vscode-azure-static-web-apps.png

以降の手順では,VSCode の Azure Static Web Apps 拡張機能の README.md に書いてある手順を実行します。

2. リソース作成

VSCode の左側タブの Azure アイコンをクリックすると,認証のためのダイアログが表示されます。認証に成功して自分のサブスクリプションが VSCode に表示されれば OK です。
そのあと + アイコンをクリックすると,リソース作成ダイアログに進みます。
create-azure-static-web-apps.png
ダイアログには下記の情報を入力します。

  • Azure のリソース名 → 例)document-site
  • デプロイするブランチ名 → 例)main
  • ソースコードの場所 → /site
  • Functions コードの場所8 → "Skip for now"
  • ビルド生成物が配置される場所 → "Skip for now"

3. GitHub Actions のワークフロー定義ファイルを確認する

↑ でリソースを作成すると,(GitHub 上の)リモートリポジトリに Azure のボットが自動でワークフロー定義ファイルを commit してきます。git pull して確認してみましょう。

すると,.github/workflows/azure-static-web-apps-{app_name}.yml というファイルがあるのが確認できると思います。

GitHub Actions のセットアップ

ワークフローの修正

先ほど,ワークフロー定義ファイルが自動生成されたのを確認しました。しかし,このままだとパイプラインは失敗します。理由は,以下のジョブが不足しているからです:

  1. GitHub Actions の仮想マシンでの Python 実行環境セットアップ
  2. mkdocs と mkdocs-material のインストール
  3. ビルドして /site ディレクトリに 静的コンテンツ (HTML,CSS,JS) を生成する処理

ということで,uses: actions/checkout@v2 以降に,それぞれのジョブを以下のように追加してみましょう

    steps:
      - uses: actions/checkout@v2

      # 1. GitHub Actions の仮想マシンでの Python 実行環境セットアップ
      - name: Set up Python 3.9
        uses: actions/setup-python@v2
        with:
          python-version: 3.9

      # 2. mkdocs と mkdocs-material のインストール
      - name: Install Python dependencies
        run: |
          python -m pip install --upgrade pip
          pip install mkdocs
          pip install mkdocs-material

      # 3. ビルドして `/site` ディレクトリに HTML,CSS,JS を生成する
      - name: Build Web App
        run: |
          mkdocs build --clean

push & deploy!

それでは更新した内容で commit & push しましょう!

git add .
git commit -m "ci: Add steps to build web app"
git push origin main

※ push すると自動で GitHub Actions が走り,Azure Static Web Apps へのデプロイが走ります。

GitHub Actions の画面を開くと,パイプラインが走っているのが見えます。
github-actions-running.png
だいたい 2分ほどでデプロイが完了します。

デプロイされたページの確認

デプロイされたリソースを Azure Portal で確認してみると...
deployed-resource.png

「概要」からウェブサイトの URL をクリックして,所望のページが見えればおkです。

おまけ:環境ごとのデプロイ

Static Web Apps では,環境ごとのデプロイを設定レスでやってくれます。

例えば,ステージング用のブランチ stage を作ってプルリクを出すと,プルリクを契機に自動でステージング環境用の静的サイトをデプロイしてくれます。試しに,プルリクを発行してみると,こんな感じになりました↓
pr.png
同じプルリク画面から,差分とその結果が確認できて便利ですし,1からこういう仕組みを作らなくても良いのがすばらしいです。

⭐ まとめ

長くなり色々書いてしまいましたが,特に難しいことをせずともドキュメントサイトをデプロイできるということが分かっていただけたのではないでしょうか!?

? おまけ: Azure Strorage Blob へのデプロイ

何らかの都合で,コードベースとして GitLab や Azure DevOps などのサービスを使わねばならないという方も多いと思います。そんな方は Azure Storage を使いましょう!Static Web Apps ほどの便利機能はありませんが,こちらもリソース費用を抑えながら静的サイトをデプロイすることができると言う点で良い選択肢です。

とりあえずデプロイするだけなら,以下の方法でできます。

1. VSCode の Azure Storage 拡張機能のインストール

スクリーンショット 2020-11-29 15.59.40.png

2. MkDocs プロジェクトのビルド

リポジトリのルートに移動して,次のコマンドを実行してビルドします。

mkdocs build --clean

すると,/site ディレクトリが生成されるので, VSCode のファイルエクスプローラからこのディレクトリを右クリックし,"Deploy to Static Website via Azure Storage..." をクリックします。

スクリーンショット 2020-11-29 16.12.02.png

すると例によってリソース作成 or 選択ダイアログが表示されるので,GUI からポチポチしていくと,無事デプロイされるかと思います。


  1. 静的サイトジェネレータ とは,Web サイトの静的コンテンツ(HTML,CSS,JS)を生成するアプリ・フレームワークのことです。生成した HTML,CSS,JS はホスティングサーバからブラウザに配信することができます。ホスティングサーバはあくまでソースコードを配信するだけであって,サーバ上で 動的に HTML を生成することがない(=HTML レンダリングのための実行環境が不要)ので,ホスティングに最適化したクラウドサービスを使うことでランニングコストを抑えることができます。静的サイトジェネレータについてもっと興味がある方は 「静的サイトジェネレーター」について網羅的に説明します - Dyno を読んでみてください。

    ちなみに,「動的サイト」(とは普通言いませんが) なるものは,ユーザーが Web サイトにアクセスした時に HTML を動的に生成するものです。これには,HTML がサーバー側・ブラウザ側のどっちで生成されるか という観点から次の2種類があります。1つは,ユーザーからアクセスが来た時に サーバー側 で HTML を動的に生成して,生成した HTML をブラウザに配信するアーキテクチャであり, SSR (Server-Side Rendering) と呼ばれます。一方,ブラウザ上 で JavaScript によって HTML を生成する Web アプリのことを SPA (Single Page Application) といいます。SPA も動的と言えば動的ですが,サーバーサイドに HTML 生成のための実行環境が不要という点では静的サイトと同じであり,Azure Static Web Apps にデプロイ可能です。
    SSRSPA静的サイト の違いをもっと知りたい!という方は,ACADEMIND, "Dynamic vs SPA vs Static Websites" を読んでみてください。 

  2. もちろん,フロントエンドのアーキテクチャにも依存しますが,サーバーサイドに HTML 生成の実行環境を必要としない 静的サイトSPA (Single Page Application) を採用する場合は最良の選択肢と言えます。 

  3. 正式版がリリースされると課金対象になる可能性がありますが,その場合でも App Service などと比較すればごく微々たるものになると想定されます。 

  4. Static Web Apps では、静的アセットは従来の Web サーバーから分離され、世界各地の地理的に分散したポイントから提供されます。 この分散により、ファイルがエンド ユーザーに物理的に近づくため、ファイルの提供が大幅に高速になります。 - Azure Document, "Azure Static Web Apps" 

  5. リソースのメンテは不要ですが,もちろんフロントエンド部分の依存パッケージ(今回は MkDocs)に関しては,必要に応じてアップデートや脆弱性対応を行う必要があります。 

  6. 近々 Azure DevOps + Azure Pipelines もサポートされると MS の人が言っています。https://github.com/Azure/static-web-apps/issues/5#issuecomment-727289989 

  7. Azure Docs, Azure Storage での静的 Web サイト ホスティング 

  8. Azure Static Web Apps で "Functions コード",または "Functions の統合" と呼ばれる機能は、文字通り Static Web Apps と Functions の統合をシームレスにできますよ!という機能です。フロントエンドで Functions で作った API を使うと何が嬉しいのでしょうか?具体的には、次のようなユースケースで役に立ちます↓

    フロントエンド開発において、バックエンドから何かしらの値をとってきて表示したいということがあります。ただし、バックエンドのインタフェースは必ずしもフロントエンドにとってうれしいものではないときがあります。そこで、中間的なバックエンドを作ってバックエンドをラップしようというアプローチがあります(このアーキテクチャを BFF(Backend for Frontend) といいます)。フロントエンド寄りの「バックエンド」なので、同じリポジトリで管理できた方が良いということでこのようなオプションが表示されるのだと思います。 

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

Dockerで始めるDjango生活(5日目)

初めに

5日目です。
4日目はこちら
前回は管理サイトを作成して、管理サイトから投稿しました。
今回はシェルから投稿しようと思います。

目次

  1. pythonシェルを開く
  2. シェルから投稿する

1. pythonシェルを開く

まずは、myprojectディレクトリに移動します。

# cd /root/projects/myproject/myproject
# pwd
/root/projects/myproject/myproject
# ls
blog  db.sqlite3  manage.py  myproject
#

それではpythonシェルを開こうと思います。

# python3 manage.py shell
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 1+2
3
>>> 2*2
4
>>>

上の様に

>>>

が表示されれば大丈夫です!

2. シェルから投稿する

では、実際に投稿しようと思います。

>>> from django.contrib.auth.models import User
>>> from blog.models import Post
>>> user = User.objects.get(username='admin')
>>> post = Post(title='Another post',
... slug='another-post',
... body='Post body.',
... author=user)
>>> post.save()
>>>

すると、管理画面を確認すると下の様にAnother Postが追加されています。image.png
この方法では、一度メモリにオブジェクトを作成してからデータベースに保存する方法です。
次の方法で実行すると一度のコマンドでオブジェクトを作成してデータベースに保存します。

>>> Post.objects.create(title='one more post',
... slug='one-more-post',
... body='Post body.',
... author=user)
<Post: one more post>
>>>

画面を確認すると新たにone more postが追加されています。
image.png

次にタイトルを更新してみます。

>>> post.title = 'New title'
>>> post.save()
>>>

再度画面を確認するとAnother postのタイトルがNew titleになっています。
image.png

3. 投稿内容を取得する

全件取得する

>>> all_posts = Post.objects.all()
>>> all_posts
<QuerySet [<Post: one more post>, <Post: New title>, <Post: テスト>]>
>>>

フィルターをかけて取得する

>>> Post.objects.filter(publish__year=2020)                           # publishの年が2020のもの
<QuerySet [<Post: one more post>, <Post: New title>, <Post: テスト>]>
>>> Post.objects.filter(publish__year=2020, author__username='admin') #年が2020かつ投稿者がadminのもの
<QuerySet [<Post: one more post>, <Post: New title>, <Post: テスト>]>
>>> Post.objects.filter(publish__year=2020) \
... .exclude(title__startswith='one')                                 #年が2020かつタイトルがoneから始まらないもの
<QuerySet [<Post: New title>, <Post: テスト>]>
>>> Post.objects.order_by('title')                                    #タイトル順(昇順)
<QuerySet [<Post: New title>, <Post: one more post>, <Post: テスト>]>
>>> Post.objects.order_by('-title')                                   #タイトル順(降順)
<QuerySet [<Post: テスト>, <Post: one more post>, <Post: New title>]>
>>>

オブジェクトを削除する

>>> post = Post.objects.get(id=1)
>>> post
<Post: テスト>
>>> post.delete()
(1, {'blog.Post': 1})
>>>

再度画面を確認するとテストの投稿が削除されています。
image.png

blogディレクトリのmodels.pyに下の修正を追記してstatusがdraftのオブジェクトを取得しないようにするオプションを作成する。

models.py
class PublishedManager(models.Manager):
    def get_queryset(self):
        return super(PublishedManager,
                     self).get_queryset()\
                          .filter(status='published')
class Post(models.Model):
    ...
    objects = models.Manager()
    published = PublishedManager()

4. 最後に

この記事では下記の本を参考に書かせていただいています。
この記事ではDjangoを実際に触って動かすことをメインに書いていますので上記のマイグレーションの詳しい説明などはこの本を読んでただければと思います。
Django 3 By Example - Third Edition

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

中国ネット通販で¥2,259(時価)で買える3次元センサーを動かしてみる

中国ネット通販で¥2,259(時価)で買える3次元センサーを動かしてみる

物体までの距離Dと動きX,Yが取れるセンサーがなんとこのお値段

筆者はQiita投稿者としてはたぶん珍しく、ドローンを作ったり飛ばしたりしています。ところで「ドローン大国」の中国では信じられないぐらいの安値で高度な機能を持ったセンサーが売っています。AliExpress で"PMW3901"を検索してみましょう。
https://github.com/r9kawai/3DSensorExperiment

image.png
※検索結果の一例です。特定の製品を勧めるものではありません。互換性を持った製品は多く存在するようです。

安いですね(金銭感覚によります)。LIDARといえば自動運転車などで知られているハイテク感ある単語。オプティカルフローもOpenCVとかGPUでやる高度な画像処理(動き検出)です。それがこんなちっぽけで、こんな値段のボードでできるんだろうか?疑うのも無理はありませんが、これは本来はドローンの底部に装着して離着陸の自動制御を行うための「ランディングセンサー」。チップが大量に使用されているため安価なのだと思われます。
 ではこれを、Pythonのコードから使うことができたら楽しいのではないでしょうか?これに挑戦してみました。
以下の記事中ではWin10 PC上のVisualStudioのPython3のコードでセンサーのデータを受信しています。UbuntuやRaspberryPi上で同じことができるはずです。シリアルポート周りの設定だけ変更が必要かと思います。

開封の儀(静電防止袋を)

image.png
ポチってから待つこと3週間。中国から送られてきたセンサーの実物です。小さい。小指より小さい。うっかりすると失くします。中国ネット通販を使うことの敷居は各自でなんとかしてください。

ジャンパピンをはんだ付けする

このセンサーモジュールはそのままでは使えません。なので、どこのご家庭にも転がっているジャンパーピンをはんだ付けしてDIPメスが刺せるようにします。
こんな風にする。
image.png

例のケーブル¥379(時価)もポチる

ラズパイ使いの皆さんならポチらなくても既に持っているでしょう。USBから3.3V-2線式シリアルと5V,GNDを取り出すあのケーブル(USB-TTLシリアルコンソール用ケーブル)が必要です。これは同等品が日本のAmazonで各種買えます。そしてこのように接続します。
赤-5V 黒-GND 緑-RX 白-TX
image.png
image.png

RaspberryPiやArduinoのピンヘッダのシリアル(3.3V系)に直接つなぐなどの方法を取れば変換ケーブルは必要なくなります。ここではWindows10 PCから制御します。センサーモジュールの電源は5Vです。消費電力はわずか(10mA)のためこれもRaspberryPiから取ることができるでしょう。

くくりつける箱も用意する(なくてもいい)

このままだとケーブルの先に落ち着かない小さな基盤がくっついていて実験がしにくいですね。くくりつける箱を3Dプリンターで作成しました。STLファイルを投稿してあるのでよければ利用してください。プラ板でも段ボールでもなんでもいいと思いますが、モジュール上のセンサーの邪魔をしないようにしましょう。カメラレンズが光学センサーで、ひじょうに小さな穴が2個開いている部品がLIDARです。そこを避ければ覆っても良いでしょう。
image.png
こういう見た目になった

ひとまず様子見で動かしてみる

 このセンサーは電源をつなぐと瞬時に起動し、シリアル上にひたすらセンシングデータを送りつづけます。それだけの一途なやつです。ファームの書き換え手段などは無いようです。まずは動いているかどうかの様子見にTeraTermでCOMポートのデータを覗いてみました。
 まずはWindowsのデバイスマネージャからUSBシリアル変換君のCOMポートの番号*を調べます。次にTeraTermをCOM*に接続し、シリアルの設定を115200bpsにします。
image.png
文字化けでぐちゃっているが、どばーっとデータが送られてきているのは確認できた
image.png
 TeraTermのログ機能を用い、バイナリ指定でデータを*.BINファイルに数秒分ほど落とします。バイナリエディタで中身を見るとこんな感じでした。
パケットらしき構造が見える。正しいデータが送られていると思われる

本筋じゃないところでハマった

 実は、USBシリアル変換ケーブルのドライバーがWin10で動作せず。ちょっとハマりました。どうかするとコードを書くよりハマりました。
上手くドライバが認識されている場合
image.png
ダメな場合こうなる
image.png
"Prolific USB Serial ドライバ"で検索すると同じようにハマる人が多いことが判ります。何故だかドライバのバージョンを切り替えたりしないと動かなかったりするようです。

MSPってどんなプロトコル?

 ようやくコードに近づきました。このセンサーモジュールはMSP(Multiwii Serial Protocol)でデータを喋り続けます。聞いたことが無い言葉ですね・・・。これはフライトコントローラと呼ばれるドローンの中枢部と、これに接続される様々なデバイス群(センサー、モータコントローラ、etc)との間で交わされる簡素な取り決めです。(筆者の主な取り組みはこちらです)
https://github.com/iNavFlight/inav/wiki/MSP-V2
image.png
 しかし、ひとまずプロトコルの正式な実装にはなるべく触れず、とにかくセンサーから送られてくるデータをPython上の変数に入れることを考えます。

たぶんこれで正しく読めている

ようやくコードが登場します。

3DSensorExperiment_test.py
import sys
import time
import struct
import threading
import serial
from serial.tools import list_ports

ports = list_ports.comports()
devices = [info.device for info in ports]
if len(devices) == 0:
        print("error: device not found")
else:
        print("found: device %s" % devices[0])
comdev = serial.Serial(devices[0], baudrate=115200, parity=serial.PARITY_NONE)
#comdev = serial.Serial('COM1', baudrate=115200, parity=serial.PARITY_NONE)

def rev_thread_func():
    while not rcv_thread_stop.is_set():
        bytesA = comdev.read(1)
        mark = int.from_bytes(bytesA, 'little')
        if mark == 36:
            bytesB = comdev.read(5)
            bytesC = comdev.read(2)
            psize = int.from_bytes(bytesC, 'little')
            bytesD = comdev.read(psize)
            bytesE = comdev.read(1)
            if psize == 5:
                val1 = int.from_bytes(bytesD[0:1], 'little')
                val2 = int.from_bytes(bytesD[1:5], 'little', signed=True)
                val3 = 0
                tabs = '\t\t'
            elif psize == 9:
                val1 = int.from_bytes(bytesD[0:1], 'little')
                val2 = int.from_bytes(bytesD[1:5], 'little', signed=True)
                val3 = int.from_bytes(bytesD[5:8], 'little', signed=True)
                tabs = '\t'
            else:
                continue
            viewtext1 = str(bytesA.hex()) + ' : ' + str(bytesB.hex()) + ' : ' + str(bytesC.hex()) + ' : ' + str(bytesD.hex()) + ' : ' + str(bytesE.hex())
            viewtext2 = '(' + str(val1) + ',' + str(val2) + ',' + str(val3) + ')'
            print(viewtext1, tabs, viewtext2)
    comdev.close()
    return

rcv_thread_stop = threading.Event()
rcv_thread = threading.Thread(target = rev_thread_func)
rcv_thread.daemon = True
rcv_thread.start()

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    rcv_thread_stop.is_set()
    rcv_thread.join(1)
    print("done")

このコードではCOMポートの一番最初の番号を開き、受信したシリアルデータをMSPパケットとして処理、val1,val2,val3の3つの変数を取り出します。生データと変数にデコードした値を標準出力に流し続けます。CTRL+Cで停止します。COMポート番号を変えたい場合、Linux系で動かす場合は、15行のコメントを参考に変更してください。

読める!読めるぞ!

実は、MSPのプロトコルであることは分かっていますが、そのペイロードの中身までは明確な仕様を見つけられませんでした、上記のコードは「9バイトと5バイトのパケットを送ってくるらしい」「奇数サイズだし、先頭は1byteの変数で、後は4byteの変数1個か2個だろう」「つまり、"8bit, 32bit" or "8bit, 32bit, 32bit"」を送ってきてるらしいぞ。「0xFFF... だったり 0x000... だったりするから符号付整数で+/-がバタついてるのであろう」という推測に戻づいて書きました。センサーが距離、X、Yを送ってきていることは分かっているので、まあ間違いないでしょう。本当は正式な仕様を見つけられるといいのですが、このセンサー、どうも最初に作った製品のクローン製品の互換製品の・・・ということを繰り返しているらしく、正式な開発元というのがよくわからない。中国の安価な電子部品とはそういうものです。
実行結果image.png

UIを付けて見やすくする

これじゃあんまりなので、Tkinterでセンサーが返してくる値をリアルタイムで表示するUIを付けました。

3DSensorExperiment_test.py
import sys
import time
import struct
import tkinter as tki
from scrolledtext import ScrolledText
import threading
import serial
from serial.tools import list_ports

title = 'Start 3DSensorExperiment'
print(title)
win = tki.Tk()
win.title(title)

def winClose():
    rcv_thread_stop.is_set()
    rcv_thread.join(1)
    win.quit()
    return

win.wm_protocol("WM_DELETE_WINDOW", winClose)

distance_val = 0
distance_str = tki.StringVar()
distance_str.set('Distance : No Sense')
distance_indicate = tki.Label(textvariable=distance_str, width=15, anchor=tki.W, justify='left',
                                         foreground='#ffffff', background='#00007f', font=("",32))
distance_indicate.pack(fill="both", anchor=tki.W)

illuminance_val = 0
illuminance_str = tki.StringVar()
illuminance_str.set('Illuminance : No Sense')
illuminance_indicate = tki.Label(textvariable=illuminance_str, width=15, anchor=tki.W, justify='left',
                                         foreground='#ffffff', background='#000000', font=("",32))
illuminance_indicate.pack(fill="both", anchor=tki.W)

xmove_val = 0
xmove_str = tki.StringVar()
xmove_str.set('  X move : No Sense')
xmove_indicate = tki.Label(textvariable=xmove_str, width=15, anchor=tki.W, justify='left',
                                         foreground='#ffffff', background='#7f0000', font=("",32))
xmove_indicate.pack(fill="both", anchor=tki.W)

ymove_val = 0
ymove_str = tki.StringVar()
ymove_str.set('  Y move : No Sense')
ymove_indicate = tki.Label(textvariable=ymove_str, width=15, anchor=tki.W, justify='left',
                                         foreground='#ffffff', background='#007f00', font=("",32))
ymove_indicate.pack(fill="both", anchor=tki.W)

txtbox_lines = 0
txtbox = ScrolledText()
txtbox.pack()

ports = list_ports.comports()
devices = [info.device for info in ports]
if len(devices) == 0:
        print("error: device not found")
else:
        print("found: device %s" % devices[0])
comdev = serial.Serial(devices[0], baudrate=115200, parity=serial.PARITY_NONE)
#comdev = serial.Serial('COM1', baudrate=115200, parity=serial.PARITY_NONE)

def set_distance(arg):
    if arg > 0 and arg < 100000:
        distance_val = arg
        str_val = 'Distance : ' + str(distance_val) + ' [mm]'
    else:
        str_val = 'Distance : No Sense'
    distance_str.set(str_val)
    return

def set_illuminance(arg):
    if arg != 0:
        illuminance_val = arg
        str_val = 'Illuminance : ' + str(illuminance_val)
        illuminance_str.set(str_val)
    return

def set_xmove(arg):
    if arg != 0:
        xmove_val = arg
        str_val = '  X move : ' + str(xmove_val) + ' [pix]'
        xmove_str.set(str_val)
    return

def set_ymove(arg):
    if arg != 0:
        ymove_val = arg
        str_val = '  Y move : ' + str(ymove_val) + ' [pix]'
        ymove_str.set(str_val)
    return

def rev_thread_func():
    while not rcv_thread_stop.is_set():
        bytesA = comdev.read(1)
        mark = int.from_bytes(bytesA, 'little')
        if mark == 36:
            bytesB = comdev.read(5)
            bytesC = comdev.read(2)
            psize = int.from_bytes(bytesC, 'little')
            bytesD = comdev.read(psize+1)
            if psize == 5:
                val1 = int.from_bytes(bytesD[0:1], 'little')
                val2 = int.from_bytes(bytesD[1:5], 'little', signed=True)
                val3 = 0
            elif psize == 9:
                val1 = int.from_bytes(bytesD[0:1], 'little')
                val2 = int.from_bytes(bytesD[1:5], 'little', signed=True)
                val3 = int.from_bytes(bytesD[5:8], 'little', signed=True)
            else:
                val1 = 0
                val2 = 0
                val3 = 0
            if psize == 5:
                set_distance(val2)
            if psize == 9:
                set_illuminance(val1)
                set_xmove(val2)
                set_ymove(val3)

            viewtext = str(bytesA.hex()) + ' : ' + str(bytesB.hex()) + ' : ' + str(bytesC.hex()) + ' : ' + str(bytesD.hex())
#           print(viewtext)
            global txtbox_lines
            if txtbox_lines >= 100:
                txtbox.delete('1.0', '2.0')
            txtbox.insert('end', viewtext + '\n')
            txtbox_lines += 1
            txtbox.see('end')
            txtbox.focus_set()
    comdev.close()
    return

rcv_thread_stop = threading.Event()
rcv_thread = threading.Thread(target = rev_thread_func)
rcv_thread.daemon = True
rcv_thread.start()

win.mainloop()

実行結果
image.png

センサーの前で手を動かしたりしてみてください。各表示値が変化するはずです。
・Distance:単位はmm、約2000mmまでは反応するが、それ以上は反応しない。実際には最下位桁はほぼ誤差。
・Illuminance:-127~128の値で、周囲の明るさ?動き検出のコンディションを示す。
・X move / Y move:単位はピクセル?で、オプティカルフローセンサーの検出値を示す。

使い道はいろいろ

距離と、XYの動きが取得できるのでいろいろな使い道があると思います。
こうした安価なセンサーが中国のドローン部品市場には豊富にあるので使いこなしてみましょう。

以上

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

PR TIMESのリリース記事をスクレイピングする

本記事では、PythonでWebスクレイピングをして、PR TIMESのリリース記事を取得するコードをご紹介します。

PR TIMESは様々なプレスリリース・ニュースリリース記事を掲載しているWebメディアです。
毎日数多くのリリース記事が発表されており、検索キーワードでリリース記事を絞り込むこともできます。

本記事のコードはGoogle Colab上で実行することを想定しています。
Google ColabはWebブラウザ上でPythonコードを実行できるサービスです。

注意事項

Webスクレイピングについての注意事項は以下の記事が参考になります。
とても良くまとまっていますので、ぜひご一読ください。

Webスクレイピングの注意事項一覧 - Qiita

法的な問題が関わって来ますので、スクレイピングする際には留意ください。

(お約束ではありますが、本記事は悪質な行為を助長するための記事ではありません。
あくまでもプログラミングやスクレイピング技術の共有を目的として公開しています。)

技術要素

  • Python3.6
  • Selenium Web Driver (Chrome Driver)
  • BeautifulSoup4
  • Google Colab

ソースコード

以下よりスクレイピングをするためのソースコードを記載して行きます。

今回PR TIMESでスクレイピングする操作は以下の通りです。

  1. PR TIMESのトップページを開く
  2. 検索キーワードを入力して検索する
  3. 検索結果から記事詳細ページを開く
  4. 記事詳細から情報を抽出する
  5. 一覧のページング(もっと見るを押下)

準備

Google Colab(以下、Colab)は専用の仮想環境を立ち上げてコードを実行します。
今回のコードで必要となるパッケージは、仮想環境上のミドルウェアには含まれていないため、
先にインストールしておく必要があります。

Colabに新規コードを作成し、以下のコードを貼り付け、実行してください。

Colabセットアップ用コード
!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
!pip install selenium

順に解説して行きます。

まず、Linux OSに含まれるパッケージのアップデートを行います。
パッケージを追加するための事前準備として既存パッケージを最新化しておきます。

!apt-get update

次にSeleniumで使用するChrome Driveをインストールします。
apt installコマンドでインストールを行うと、/usr/lib/chromium-browser配下に
Chrome Driveの本体が格納されます。
このままだと実行できないので、/usr/binに本体をコピーします。

!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin

pipはPythonのパッケージをインストールするためのコマンドです。
Seleniumパッケージをインストールします。

!pip install selenium

プレーンなコード

オブジェクト指向を用いないプレーンなコードを掲載します。
そこまで長くはないので、こちらのコードだけでも読めるかと思います。

後述していますが、折角なのでオブジェクト志向的に部品化したコードも掲載しました。
そちらの方が見通しは良くなっていますので、処理の大枠の流れはつかみやすいかと思います。

リリース記事のスクレイピング
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from urllib import request
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import datetime
import time
import requests

#############
## 以下、実行前に変更する
#############
# いつからの分を取得するか指定する(開始日付 YYYY-MM-DD)
START_DT_STR = '2020-12-01'
# 検索キーワード
SEARCH_WORD = '検索キーワード'
############

start_dt = datetime.datetime.strptime(START_DT_STR, '%Y-%m-%d')

#webdriveの設定
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')

#webdriverを起動
driver = webdriver.Chrome('chromedriver',options=options)

#PR TIMESのトップページを開く
target_url = 'https://prtimes.jp/'   
driver.get(target_url)

#検索欄をクリックする   
driver.find_element_by_xpath("/html/body/header/div/div[4]/div/input").click()
#検索バーにキーワードを入れ、クリックする
kensaku = driver.find_element_by_xpath("/html/body/header/div/div[4]/div/input")
kensaku.send_keys(SEARCH_WORD)
kensaku.send_keys(Keys.ENTER)

cnt = 0
while True:
  #記事数を出力(進捗確認用)
  print(len(driver.find_elements_by_xpath('/html/body/main/section/section/div/article')))

  #もっとみるを押す
  try:
    driver.find_element_by_xpath("/html/body/main/section/section/div/div/a").click()
  except: 
    break

  html = driver.page_source
  soup = BeautifulSoup(html, "html.parser")

  #記事URLを取得(40件ずつ処理)
  articles = soup.find_all(class_='list-article__link')[cnt*40:]

  #記事情報を格納する配列
  records = []

  #終了フラグ
  eof_flag = False

  #記事ごとの情報を取得
  for article in articles:
    article_time = article.find(class_='list-article__time')
    #print(article_time)

    #記事公開日時をdatetime表記に変換
    try:
      str_to_dt = datetime.datetime.strptime(article_time.get('datetime'), '%Y-%m-%dT%H:%M:%S%z')
    except:
      try:
        article_time_cvt = article_time.get('datetime').replace('+09:00', '+0900')
        str_to_dt = datetime.datetime.strptime(article_time_cvt, '%Y-%m-%dT%H:%M:%S%z')
      except:
        str_to_dt = datetime.datetime.strptime(article_time.text, '%Y年%m月%d日 %H時%M分')
    article_time_dt = datetime.datetime(str_to_dt.year, str_to_dt.month, str_to_dt.day, str_to_dt.hour, str_to_dt.minute)

    #開始日付より前であれば終了(記事は最新日付の順でソートされているため)
    if article_time_dt < start_dt:
      eof_flag = True
      break

    relative_href = article["href"]
    url = urljoin(target_url, relative_href)
    #print(url)

    #URLを1記事ずつ開く
    r = requests.get(url)
    html = r.text
    soup = BeautifulSoup(html, "html.parser")

    #記事タイトル
    title = soup.select_one("#main > div.content > article > div > header > h1").text
    #記事サブタイトル
    sub_title_elem = soup.select_one("#main > div.content > article > div > header > h2")
    if sub_title_elem:
      sub_title = sub_title_elem.text
    else:
      sub_title = ""

    #会社名
    company = soup.select_one("#main > div.content > article > div > header > div.information-release > div > a").text
    #記事公開日
    published = soup.select_one("#main > div.content > article > div > header > div.information-release > time").text
    #記事本文
    content = soup.select_one('#main > div.content > article > div > div.rbody').text

    #配列に記事の情報を追加
    records.append({
        'url': url,
        'title': title,
        'sub_title': sub_title,
        'content': content,
        'company': company,
        'published': published
    })

  if records:
    #print('DBへの登録処理など')
    pass

  #終了フラグがTrueになっている時はループを抜ける
  if eof_flag:
    break

  #2秒間待つ
  time.sleep(2)  
  cnt += 1


部品化したコード

見通しを良くするために部品化したコードも掲載しておきます。
※クラス定義の部分は別枠に抜粋しましたので、実行する際には「クラス定義」の部分に差し込んでください。

リリース記事のスクレイピング
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from urllib import request
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import datetime
import time
import requests

#############
## 以下、実行前に変更する
#############
# いつからの分を取得するか指定する(開始日付 YYYY-MM-DD)
START_DT_STR = '2020-12-01'
# 検索キーワード
SEARCH_WORD = '検索キーワード'
############

#############
## 定数定義
#############
# PR TIMESのURLプレフィックス
PRTIMES_URL = 'https://prtimes.jp/'

#############
## クラス定義
#############
# ※後述の「クラス定義」に記載

#############
## メイン処理
#############
# 開始日付
start_dt = datetime.datetime.strptime(START_DT_STR, '%Y-%m-%d')

# driverを取得(検索キーワードで検索済みの状態)
driver_factory = PRTimesWebdriverFactory(PRTIMES_URL)
driver = driver_factory.create(SEARCH_WORD)

page_operator = PageOperator(driver)

cnt = 0

while True:
  #記事数を出力(進捗確認用)
  print(page_operator.get_article_length())

  #もっとみるを押す
  if page_operator.more() == False:
    break

  #記事URLを取得(40件ずつ処理)
  articles = page_operator.parse().get_articles(cnt)

  #記事情報を格納する配列
  records = []
  #終了フラグ
  eof_flag = False

  #記事ごとの情報を取得
  for article in articles:
    article_operator = ArticleOperator(PRTIMES_URL, article)

    #記事の日付を取得
    article_dt = article_operator.get_article_date()
    #開始日付より前であれば終了(記事は最新日付の順でソートされているため)
    if article_dt < start_dt:
      eof_flag = True
      break

    #記事詳細の情報を取得
    article_info = article_operator.get_detail_info()
    #配列に記事の情報を追加
    records.append(article_info)

  #追加する記事があれば処理
  if records:
    #print('DBへの登録処理など')
    #print(records)
    print('取得記事数:', len(records))
    pass

  #終了フラグがTrueになっている時はループを抜ける
  if eof_flag:
    break

  #2秒間待つ
  time.sleep(2)  
  cnt += 1

ここより「クラス定義」の部分の内容を記載します。

作成したクラスは以下の3つです。

  • PRTimesWebdriverFactory
    PR TIMESのページ操作をするためのdriverを作成するためのファクトリクラスです。
    検索キーワードで検索し、そのdriverを返却します。

  • PageOperator
    検索結果ページを操作するためのクラスです。
    ページ内の記事を取得、記事を追加で読み込む、といった機能を持たせています。

  • ArticleOperator
    詳細記事ページを操作するためのクラスです。
    ページを開き、詳細記事内の情報を取得、といった機能を持たせています。

Qiita_PRTIMESスクレイピング_クラス図.png

クラス定義
class PRTimesWebdriverFactory:
  """PR TIMES用のdriverを取得
  """
  def __init__(self, target_url):
    """コンストラクタ

    :param target_url: PR TIMESのURL
    :type target_url: string
    """
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')

    driver = webdriver.Chrome('chromedriver',options=options)

    self.target_url = target_url
    driver.get(target_url)

    self.driver = driver

  def create(self, keyword):
    """driverを作成して返却

    :param keyword: 検索キーワード
    :type keyword: string
    :return: driverオブジェクト
    :rtype: driver: selenium.webdriver.Chrome
    """
    driver = self.driver

    #検索欄をクリックする   
    driver.find_element_by_xpath("/html/body/header/div/div[4]/div/input").click()
    #検索バーにキーワードを入れ、クリックする
    kensaku = driver.find_element_by_xpath("/html/body/header/div/div[4]/div/input")
    kensaku.send_keys(SEARCH_WORD)
    kensaku.send_keys(Keys.ENTER)

    return driver

class PageOperator:
  """ページ操作クラス
  """
  def __init__(self, driver):
    """コンストラクタ

    :param driver: driverオブジェクト
    :type driver: selenium.webdriver.Chrome
    """
    self.driver = driver

  def get_article_length(self):
    """ページ内の記事数を取得
    """
    driver = self.driver

    return len(driver.find_elements_by_xpath('/html/body/main/section/section/div/article'))

  def more(self):
    """もっと見る
    """
    driver = self.driver

    try:
      driver.find_element_by_xpath("/html/body/main/section/section/div/div/a").click()
      return True
    except: 
      return False

  def parse(self):
    """現在ページをパース
    """
    driver = self.driver

    html = driver.page_source
    self.soup = BeautifulSoup(html, "html.parser")

    # チェーンメソッドのためのリターン
    return self

  def get_articles(self, cnt, size=40):
    """ページ内の記事を取得

    :param cnt: ページ数
    :type cnt: int
    :param size: ページあたりの記事数
    :type size: int, optional, defaults to 40
    :return ページ内の記事要素の配列
    :rtype list
    """
    soup = self.soup

    articles = soup.find_all(class_='list-article__link')[cnt*size:]
    #print(len(articles))
    return articles

class ArticleOperator:
  """記事操作クラス
  """
  def __init__(self, target_url, article):
    """コンストラクタ

    :param target_url: PR TIMESのURL
    :type target_url: string
    :param article: 記事要素
    :type bs4.element.Tag
    """
    self.target_url = target_url
    self.article = article

  def get_article_date(self):
    """記事公開日を取得

    :return 記事公開日
    :rtype datetime.datetime
    """
    article = self.article

    article_time = article.find(class_='list-article__time')
    #記事公開日時をdatetime表記に変換
    try:
      str_to_dt = datetime.datetime.strptime(article_time.get('datetime'), '%Y-%m-%dT%H:%M:%S%z')
    except:
      try:
        article_time_cvt = article_time.get('datetime').replace('+09:00', '+0900')
        str_to_dt = datetime.datetime.strptime(article_time_cvt, '%Y-%m-%dT%H:%M:%S%z')
      except:
        str_to_dt = datetime.datetime.strptime(article_time.text, '%Y年%m月%d日 %H時%M分')
    article_dt = datetime.datetime(str_to_dt.year, str_to_dt.month, str_to_dt.day, str_to_dt.hour, str_to_dt.minute)
    #print(article_dt)
    return article_dt

  def open_detail(self):
    """詳細ページを開く
    """
    article = self.article

    relative_href = article["href"]
    url = urljoin(self.target_url, relative_href)
    #print(url)

    #詳細ページを開く
    r = requests.get(url)
    html = r.text
    soup = BeautifulSoup(html, "html.parser")

    self.soup = soup

  def get_detail_info(self):
    """詳細ページの情報を取得

    :return 詳細ページの情報オブジェクト
    :rtype dict
    """
    self.open_detail()

    soup = self.soup

    # 記事タイトル
    title = soup.select_one("#main > div.content > article > div > header > h1").text
    # 記事サブタイトル
    sub_title_elem = soup.select_one("#main > div.content > article > div > header > h2")
    if sub_title_elem:
      sub_title = sub_title_elem.text
    else:
      sub_title = ""

    # 会社名
    company = soup.select_one("#main > div.content > article > div > header > div.information-release > div > a").text
    # 記事公開日
    published = soup.select_one("#main > div.content > article > div > header > div.information-release > time").text
    # 記事本文
    content = soup.select_one('#main > div.content > article > div > div.rbody').text

    # 記事情報のdictを返却
    return {
        'url': url,
        'title': title,
        'sub_title': sub_title,
        'content': content,
        'company': company,
        'published': published
    }

まとめ

PythonでPR TIMESの記事をスクレイピングするコードをご紹介しました。

ページ表示、入力、クリックなど、基本的な操作は盛り込んでありますので、
他のサイトにも応用できるコードになっています。

読んでいただいて、ありがとうございました。

===

弊社は業務効率化・自動化など、仕組みで解決するお手伝いをさせていただいております。
お仕事のご依頼はコチラ↓までお願いいたします。

株式会社シクミヤ
note: Visionary Base編集部
Twitter: @shikumiya_hata

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

普段使ってるPCA、数学的には何やってるんだっけ

TL;DR

特徴次元数削減アルゴリズムの代表格といえば主成分分析 (PCA : Principal Component Analysis) ですね。ただ、やってることは感覚的に何となく分かっていても、線形代数的に何やってるのかをよく覚えておらず、気になったので復習してみた記事です。

PCAを手計算で実装し、結果をscikit-learnのPCAサブパッケージのものと比較します。(手計算といっても sklearn.decomposition.PCA を使わないでという意味であってNumPyは使います)

主成分分析の概要

高次元の特徴からなるデータの次元削減・データ圧縮等を目的として、データをより少数の主成分と呼ばれる合成変数で記述します。高次元の特徴空間におけるデータのばらつき(分散)をより低次元な空間において出来るだけ再現するため、「主成分の分散最大化」と「主成分の直行化」が必要必要な手続きになります。これは固有値固有ベクトルを求めることにより実現されます。

実装

まずは必要なライブラリをインポートします。

In
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.decomposition import PCA  # 結果比較用

データの読込み

データセットは何でも良いですが、そこそこ特徴次元数のある breast_cancer を使うことにします。(特徴次元数 : 30)
Bunch型のデータから使用する部分を抽出します。

In
breast_cancer = load_breast_cancer()

data = pd.DataFrame(breast_cancer.data, columns=breast_cancer.feature_names)
data.head()

スクリーンショット 2020-11-30 15.38.37.png

正規化

単位が不揃いで値に大きなばらつきがあるため、正規化を行います。

In
data_stats = data.describe().transpose()

def norm(x):
    return (x-data_stats['mean'])/data_stats['std']

data = norm(data)

手計算用にNumPy配列に変換します。

In
data_array = data.values
print(data_array)
print(data_array.shape)
Out
[[ 1.09609953 -2.0715123   1.26881726 ...  2.2940576   2.74820411  1.93531174]
 [ 1.82821197 -0.35332152  1.68447255 ...  1.08612862 -0.24367526  0.28094279]
 [ 1.5784992   0.45578591  1.56512598 ...  1.95328166  1.15124203  0.20121416]
 ...
 [ 0.70166686  2.04377549  0.67208442 ...  0.41370467 -1.10357792 -0.31812924]
 [ 1.83672491  2.33440316  1.98078127 ...  2.28797231  1.9173959   2.21768395]
 [-1.80681144  1.22071793 -1.81279344 ... -1.7435287  -0.04809589 -0.75054629]]
(569, 30)

分散共分散行列の計算

変数$j$の分散$s_{j, j}$, および変数$j$と変数$k$の共分散$s_{j, k} (j \neq k)$はそれぞれ次のように表されます。

$$s_{j, j}=\frac{1}{n-1}\sum_{i=1}^{n}(x_{i, j}-\bar{x}_{.j})^2$$

$$s_{j, k}=\frac{1}{n-1}\sum_{i=1}^{n}(x_{i, j}-\bar{x_{.j}})(x_{i, k}-\bar{x_{.k}})$$

したがって、分散共分散行列$S$は次のように求められます。

In
n_instances = data_array.shape[0]
n_features = data_array.shape[1]
data_mean = np.mean(data_array, axis=0)

S = np.zeros((n_features, n_features), dtype=float)
for j in range(n_features):
    for k in range(n_features):
        for i in range(n_instances):
            S[j][k] += (data_array[i][j] - data_mean[j])*(data_array[i][k] - data_mean[k]) / n_instances

固有値・固有ベクトルの計算

続いて$S$ の固有値・固有ベクトルを計算します。
固有値・固有ベクトルの計算には numpy.linalg.eig を利用します。

In
# 固有値
eigenvalues = np.linalg.eig(S)[0]
# 固有ベクトル
U = np.linalg.eig(S)[1]

print(eigenvalues)
Out
array([1.32582657e+01, 5.68135223e+00, 2.81299652e+00, 1.97715956e+00, 1.64583295e+00, 1.20523472e+00, 6.74033435e-01, 4.75779500e-01, 4.16162133e-01, 3.50077124e-01, 2.93399148e-01, 2.60702387e-01, 2.40933318e-01, 1.56733784e-01, 9.39695257e-02, 7.97224445e-02, 5.92946458e-02, 5.25263076e-02, 4.93906364e-02, 1.32811001e-04, 7.47487099e-04, 1.58654466e-03, 6.88833652e-03, 8.16326791e-03, 1.54540635e-02, 1.80232759e-02, 2.42980595e-02, 2.73911786e-02, 3.11046408e-02, 2.99202175e-02])

これで元データを混合変数へと変換する行列$U$を求めることができました。
ここで得られた$U$は、次で表されるようにように$S$を対角化する行列です。

$$U^TSU=diag(λ1, ..., λ_p)$$

In
U_inv = np.linalg.inv(U)
diag = np.dot(np.dot(U_inv, S), U)
diag
Out
array([[ 1.32582657e+01,  1.27416995e-15,  2.59655658e-16, -1.25261426e-15,  4.04168592e-16, -6.09310220e-16, -1.74696331e-15,  2.38945931e-15, -4.38301407e-16, -1.20268061e-15,  3.54961084e-16, -6.99035253e-16,  3.30506578e-16, -2.56258925e-16,  2.75352231e-15,  2.24139149e-16, -1.33023827e-15,
        -6.37619774e-16,  3.22970991e-15,  4.21239268e-15,  3.08658922e-15, -2.76021803e-15,  2.97518279e-15,  9.45346327e-17,  1.06484694e-15, -7.14021028e-16,  2.37497868e-15, -1.50135500e-15, -5.45871698e-16, -1.40709743e-15],
       [ 8.80527252e-16,  5.68135223e+00, -2.31827482e-16,  1.37268856e-16, -9.73260548e-16, -4.09033288e-16, -4.35578462e-16,  8.43022527e-17, -2.69433268e-16, -9.23904427e-16,  1.19330641e-15, -1.07633683e-15,  7.72295909e-16,  2.10526158e-16, -4.86324918e-16,  1.17279288e-15,  1.39606877e-16,
         8.77355419e-16,  1.43290742e-17, -6.60369435e-16, -1.41242614e-15,  1.84603892e-15,  2.74993617e-16, -2.57557446e-16,  4.23073643e-16, -1.21349087e-16,  3.49565329e-16,  1.95562317e-17,  2.80092695e-16,  1.42138866e-16],
       ...
       [ 2.11188838e-15,  5.39314735e-16,  1.63313324e-16, -6.34796943e-16,  3.03143545e-16, -1.45993795e-16, -1.09593888e-16,  1.35971780e-16, -1.47104768e-17,  5.63829035e-17, -2.31515095e-16,  4.54289262e-17,  4.46706707e-17,  1.42878079e-17, -6.69645307e-17,  2.12551787e-17, -9.76503610e-17,
        -9.83490621e-17,  1.30410900e-16, -3.26122703e-17, -4.39348644e-17,  3.27142835e-17,  6.77437666e-18,  1.41538219e-17, -8.21726757e-17,  1.75046448e-17, -6.75116428e-17, -1.40021512e-17,  3.11046408e-02, -3.80548301e-17],
       [ 1.81691436e-15, -4.19837541e-16,  1.69790524e-16, -1.32978080e-15, -2.92109118e-17, -9.51987993e-17, -3.02233535e-16,  1.12616692e-16, -7.39060669e-17,  5.25773657e-17, -3.85867511e-17,  1.17268833e-16, -9.59743723e-18,  1.11617116e-17, -1.12287607e-16, -8.42366298e-18,  9.72260224e-17,
         1.10219780e-16,  1.01366564e-17,  2.55796481e-16, -2.68489853e-17,  1.20429197e-16,  1.10363419e-16, -1.23429551e-17,  6.66921317e-17,  3.95025490e-17, -3.87011501e-17, -8.63280591e-17,  2.40399949e-17,  2.99202175e-02]])

対角成分以外は0、対角成分は先に求めた固有値に一致していることが確認できます。

寄与率の計算

第$j$成分の寄与率 (contribution ratio) は次の式で求められます。各固有値の総和に対する割合と言えます。

$$c_j = \frac{λj}{λ1+ ... +λ_p}$$

In
contribution_ratios = []
sum_eigen = np.sum(eigenvalues)

for i in range(eigenvalues.shape[0]):
    contribution_ratio = eigenvalues[i] / sum_eigen
    contribution_ratios.append(contribution_ratio)

print(contribution_ratios)
Out
[0.44272025607526355, 0.1897118204403308, 0.09393163257431368, 0.06602134915470165, 0.054957684923462646,
 0.040245220398833485, 0.02250733712982507, 0.015887238000213286, 0.013896493745591104, 0.0116897818941315,
 0.009797189875980144, 0.008705379007378822, 0.008045249871967327, 0.005233657454926348, 0.0031378321676274047,
 0.0026620933651523055, 0.0019799679253242703, 0.001753959450226365, 0.001649253059225152, 4.4348274273267065e-06,
 2.4960103246860732e-05, 5.297792903810818e-05, 0.00023001546250597875, 0.00027258799547752054, 0.0005160423791651501,
 0.0006018335666716387, 0.000811361258899119, 0.0009146467510543492, 0.0010386467483387103, 0.0009990964637002163]

累積寄与率とともに寄与率を可視化してみます。

In
plt.figure()

left = range(1, len(contribution_ratios)+1)
plt.bar(left, contribution_ratios, width=1.0)
plt.plot(left, cumulated_contribution_ratios, c='black')
plt.ylim([0, 1])
plt.show()

スクリーンショット 2020-11-30 16.40.23.png

scikit-learnパッケージとの比較

scikit-learnパッケージを使えば上記の計算が2行です。

pca = PCA(n_components=30)
pca.fit(data)

fit後の explained_variance_ratio_ 属性が寄与率に相当し、手計算の結果と一致していることが分かります。

In
pca.explained_variance_ratio_
Out
array([4.42720256e-01, 1.89711820e-01, 9.39316326e-02, 6.60213492e-02, 5.49576849e-02, 4.02452204e-02, 2.25073371e-02, 1.58872380e-02, 1.38964937e-02, 1.16897819e-02, 9.79718988e-03, 8.70537901e-03, 8.04524987e-03, 5.23365745e-03, 3.13783217e-03, 2.66209337e-03, 1.97996793e-03, 1.75395945e-03,
       1.64925306e-03, 1.03864675e-03, 9.99096464e-04, 9.14646751e-04, 8.11361259e-04, 6.01833567e-04, 5.16042379e-04, 2.72587995e-04, 2.30015463e-04, 5.29779290e-05, 2.49601032e-05, 4.43482743e-06])

固有ベクトルはというと、components_属性に逆行列として存在するようです。
こちらも手計算で求めたU_invに一致します。

In
pca.components_
Out
array([[ 2.18902444e-01,  1.03724578e-01,  2.27537293e-01,  2.20994985e-01,  1.42589694e-01,  2.39285354e-01,  2.58400481e-01,  2.60853758e-01,  1.38166959e-01,  6.43633464e-02,  2.05978776e-01,  1.74280281e-02,  2.11325916e-01,  2.02869635e-01,  1.45314521e-02,  1.70393451e-01,  1.53589790e-01,
         1.83417397e-01,  4.24984216e-02,  1.02568322e-01,  2.27996634e-01,  1.04469325e-01,  2.36639681e-01,  2.24870533e-01,  1.27952561e-01,  2.10095880e-01,  2.28767533e-01,  2.50885971e-01,  1.22904556e-01,  1.31783943e-01],
       [-2.33857132e-01, -5.97060883e-02, -2.15181361e-01, -2.31076711e-01,  1.86113023e-01,  1.51891610e-01,  6.01653628e-02, -3.47675005e-02,  1.90348770e-01,  3.66575471e-01, -1.05552152e-01,  8.99796818e-02, -8.94572342e-02, -1.52292628e-01,  2.04430453e-01,  2.32715896e-01,  1.97207283e-01,
         1.30321560e-01,  1.83848000e-01,  2.80092027e-01, -2.19866379e-01, -4.54672983e-02, -1.99878428e-01, -2.19351858e-01,  1.72304352e-01,  1.43593173e-01,  9.79641143e-02, -8.25723507e-03,  1.41883349e-01,  2.75339469e-01],
       ...
       [ 2.11460455e-01, -1.05339342e-02,  3.83826098e-01, -4.22794920e-01, -3.43466700e-03, -4.10167739e-02, -1.00147876e-02, -4.20694931e-03, -7.56986244e-03,  7.30143287e-03,  1.18442112e-01, -8.77627920e-03, -6.10021933e-03, -8.59259138e-02,  1.77638619e-03,  3.15813441e-03,  1.60785207e-02,
        -2.39377870e-02, -5.22329189e-03, -8.34191154e-03, -6.35724917e-01,  1.72354925e-02,  2.29218029e-02,  4.44935933e-01,  7.38549171e-03,  3.56690392e-06, -1.26757226e-02,  3.52404543e-02,  1.34042283e-02,  1.14776603e-02],
       [-7.02414091e-01, -2.73661018e-04,  6.89896968e-01,  3.29473482e-02,  4.84745766e-03, -4.46741863e-02, -2.51386661e-02,  1.07726530e-03,  1.28037941e-03,  4.75568480e-03,  8.71109373e-03,  1.07103919e-03, -1.37293906e-02, -1.10532603e-03,  1.60821086e-03, -1.91562235e-03,  8.92652653e-03,
         2.16019727e-03, -3.29389752e-04, -1.79895682e-03,  1.35643056e-01, -1.02053601e-03, -7.97438536e-02, -3.97422838e-02, -4.58327731e-03,  1.28415624e-02, -4.02139168e-04,  2.28844179e-03, -3.95443454e-04, -1.89429245e-03]])

参考文献

  1. 日本統計学会公式認定 統計検定準1級対応 統計学実践ワークブック
  2. 主成分分析の基礎 - inoccu.com
  3. sklearn.decomposition.PCA - scikit-learn.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ArduinoのセンサモジュールをPythonに反映させてみた

ArduinoのセンサモジュールをPythonに反映させてみた

授業でpythonでゲームを作ることになったので、コントローラをArduinoで自作しようと考えたのですが、細かい記事が見当たらなかったので投稿してみました。

参考記事

https://omoroya.com/arduino-lesson13/
https://shizenkarasuzon.hatenablog.com/entry/2018/09/29/180625#5byte%E5%9E%8B%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%97%E4%BF%A1

環境

・Arduino UNO
・Arduino IDE 1.8.13
・2軸ジョイスティックモジュール
https://akizukidenshi.com/catalog/g/gM-08763/
・python 3
・pyserial 3.5

Arduino側

const int X_PIN = A0;  // X軸方向の入力をアナログピンA0に
const int Y_PIN = A1;  // Y軸方向の入力をアナログピンA1に
const int SW_PIN = 2;  // センタースイッチの入力をデジタルピンD2に

byte X_POS ;   // X軸方向の読み取り値の変数をbyte型に
byte Y_POS ;   // Y軸方向の読み取り値の変数をbyte型に
byte SW_POS ;  // センタースイッチの読み取り値の変数をbyte型に

 void setup(){

   pinMode(X_PIN, INPUT);          // A0ピンを入力に(省略可)
   pinMode(Y_PIN, INPUT);          // A1ピンを入力に(省略可)
   pinMode(SW_PIN, INPUT_PULLUP);  // D2ピンをプルアップして入力に

   Serial.begin(115200); // シリアル通信の開始
   //Serial.begin(9600); 

}


 void loop(){

   X_POS = analogRead(X_PIN)/5;     // X軸方向のアナログ値を読み取る
   Y_POS = analogRead(Y_PIN)/5;     // Y軸方向のアナログ値を読み取る
   SW_POS = 1-digitalRead(SW_PIN);  // センタースイッチの状態を読み取る

   Serial.write('H');               //ヘッダー
   Serial.write(X_POS);
   Serial.write(Y_POS);
   Serial.write(SW_POS);


   delay(100);

 }

Arduinoとジョイスティックの接続についてはたくさん記事があるので省略します。
工夫した点は2つ。
1つ目はSerial.writeでシリアル通信にセンサー値を載せるときにbyte型にします。多くのサイトではArduino IDEのシリアルレポート機能を使うためにint型にしていますが、シリアル通信はbyte型でのやりとりが楽です。Serial.printにしてstring型で送ることも可能。
2つ目はヘッダーを付加することです。pythonでシリアル通信のデータを受け取る際、1つ目のデータはX座標、2つ目はY座標3つ目はスイッチと受け取りますが、通信ラグを考えると1つ目のデータはY座標、2つ目はスイッチ、3つ目はX座標と入れ替わった順番で送られてくる可能性があるため、ヘッダーを付加しました。

Python側

Pyserialを使います。

pip install pyserial

ゲーム内のコードがこちら。

import serial
class Game:
    def __init__(self):
        self.ser = serial.Serial('COM3', 115200, timeout = 0.1)
        self.serialheader='H'.encode('utf-8')

    def __del__(self):
        self.ser.close()

    def keyboard(self):
        while True:
            if self.ser.read() == self.serialheader:#ヘッダーを受け取った時、シリアル通信のデータを受け取る
                switchflag = False
                    joyx = int.from_bytes(self.ser.read() , 'big')#x座標を読み取ろ
                    joyy = int.from_bytes(self.ser.read() , 'big')#y座標を読み取り
                    switch=self.ser.read()                        #スイッチを読み取り
                    if  switch == b'\x01':             
                        switchflag=True
                    else:
                        switchflag=False

pyserialの基本的な使い方をしています。serial.Serialの引数ですが、pyserialのバージョンによってポートの書き方が異なるそうです。自分の場合は'COM+番号'(デバイスマネージャーに表示されるやつ)でした。

ヘッダーを受け取ったとき、シリアル通信を受信した順にX座標、Y座標、スイッチとデータを受け取ります。
また、繰り返し処理が重くならないように、スイッチのバイナリとヘッダーに関しては、あらかじめbyte型にencodeしたもので論理文を作成し、decodeの手間を極力省きました。

以上拙いものではございますが、Arduinoでジョイスティックの操作をpythonのゲームに反映させることができたので共有しておきます。

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

【初心者必読】VGG16を動かしたいなら、まずはColabが正解です。

〜本記事でわかること〜

・VGG16の実装方法

1.はじめに

「わたしの備忘録」 兼 「初心者向け」の内容です。

・Deep Learningって分かんないけど、とにかく動かしたい!
・原理とかは後回し、実装方法だけ知りたい!

って方は、ぜひ読んでみてください!

2.動作環境

動作環境は以下の通り。

・Windows 10
・Google Colab
・tensorflow ver.2.3

今回はGoogle Colabにて
コーディングしていくので、
OSとかは特に関係ないです。

ネット環境だけ整ってれば、とりあえずオーケー!

3.前準備

本記事で取り上げるVGG16というモデルは、
深層学習の中でも「画像の分類」が得意です。

なので、まずは皆さんに
自前のデータを用意してほしいんですね。

わたしはマーベル作品が大好きなので、
「キャプテンアメリカ」
「アイアンマン」
「ハルク」
「ソー」
の画像を用意しました!

画像の枚数は適当でいいのですが、
手始めに各50枚ほど用意してみましょう。

用意できましたら、
VGG16が学習しやすいように
フォルダを作っていきます。

というわけで、
以下のようにフォルダを作ってください。

    image/
       ├ train/
       │    ├ captain/
       │    ├ ironman/
       │    ├ hulk/
       │    └ thor/   
       │
       ├ valid/
       │    ├ captain/
       │    ├ ironman/
       │    ├ hulk/
       │    └ thor/   
       │
       └ test/
            ├ captain/
            ├ ironman/
            ├ hulk/
            └ thor/   

そうしましたら、
「train」・「valid」・「test」フォルダの
各「captain」・「ironman」・「hulk」・「thor」フォルダに
画像を入れていきます。

各ヒーローごとに50枚あるので、
「train」に30枚、「valid」に10枚、「test」に10枚
入れていきます。

なので、全体の画像配置は
train:150枚
valid: 50枚
test : 50枚
になりますね。

前準備はこれで終了。

4.実装

4−1 まずは、Google Driveへ

Google Colabでコーディングしていくのですが
そのためにも、まずGoogle Driveへ移動します。

みなさん、現代っ子のはずなので、
Googleアカウントは、さすがに持ってますよね??
(無ければこの際に作っちゃいましょ!)

Googleアカウントにログインできましたら、
Google Driveへアクセス。

先程作った「image」フォルダをアップロードします。

4−2 実際にコードを書いてみよう

現時点では、おそらくGoogle Colabが
インストールされていないと思うので、

下に示した図のようにインストールしていきます。

4−3 学習開始!

それでは、学習していきましょう。
まずは、Google DriveとGoogle Colabを接続します。

from google.colab import drive
drive.mount('/content/drive')

そして、VGG16のモデルを作成していきます。

import matplotlib
import numpy as np
%matplotlib inline
from tensorflow.python.keras.callbacks import TensorBoard
from tensorflow.python.keras.applications.vgg16 import VGG16

vgg16 = VGG16(include_top=False, 
              input_shape=(224, 224, 3),
              weights='imagenet')

クラスの設定をします。
ここは皆さんの用意したデータによって変えてください。

import random
classes = ['captain', 'ironman', 'hulk', 'thor']

モデルの生成とコンパイルをします。

from tensorflow.python.keras.models import Sequential, Model
from tensorflow.python.keras.layers import Dense, Dropout, Flatten, GlobalAveragePooling2D
from tensorflow.keras.optimizers import SGD

def build_transfer_model(vgg16):
  model = Sequential(vgg16.layers)
  for layer in model.layers[:15]:
    layer.trainable = False
  model.add(Flatten())
  model.add(Dense(256, activation='relu'))
  model.add(Dropout(0.5))
  model.add(Dense(len(classes), activation='softmax'))
  return model

model = build_transfer_model(vgg16)

model.compile(loss='categorical_crossentropy',
              optimizer=SGD(lr=1e-5, momentum=0.9),
              metrics=['accuracy']
)

ここでは、ジェネレータとイテレータを生成します。
カンタンに言うと、画像の拡張やスケール変換をしています。

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.python.keras.applications.vgg16 import preprocess_input

idg_train = ImageDataGenerator(rescale=1/255.,
                               shear_range=0.1,
                               zoom_range=0.1,
                               horizontal_flip=True,
                               preprocessing_function=preprocess_input
)

idg_valid = ImageDataGenerator(rescale=1/255.)

img_itr_train = idg_train.flow_from_directory(directory='/content/drive/My Drive/image/train/',
                                              target_size=(224, 224),
                                              color_mode='rgb',
                                              classes=classes,
                                              batch_size=1,
                                              class_mode='categorical',
                                              shuffle=True
)

img_itr_validation = idg_valid.flow_from_directory(directory='/content/drive/My Drive/image/valid/',
                                                   target_size=(224, 224),
                                                   color_mode='rgb',
                                                   classes=classes,
                                                   batch_size=1,
                                                   class_mode='categorical',
                                                   shuffle=True
)

モデル保存用のディレクトリの準備をします。

import os
from datetime import datetime

model_dir = os.path.join('/content/drive/My Drive/image/',
                         datetime.now().strftime('%y%m%d_%H%M')
)

os.makedirs(model_dir, exist_ok = True)
print('model_dir:', model_dir)

dir_weights = os.path.join(model_dir, 'weights')
os.makedirs(dir_weights, exist_ok = True)

学習の途中経過を保存したり、保存方法を設定します。

from tensorflow.python.keras.callbacks import CSVLogger, ModelCheckpoint
import math
file_name='vgg16_fine'
batch_size_train=1
batch_size_validation=1

steps_per_epoch=math.ceil(img_itr_train.samples/batch_size_train
)
validation_steps=math.ceil(img_itr_validation.samples/batch_size_validation
)

cp_filepath = os.path.join(dir_weights, 'ep_{epoch:02d}_ls_{loss:.1f}.h5')

cp = ModelCheckpoint(cp_filepath,
                     monitor='loss',
                     verbose=0,
                     save_best_only=False,
                     save_weights_only=True,
                     mode='auto',
                     save_freq=5
)
csv_filepath = os.path.join(model_dir, 'loss.csv')
csv = CSVLogger(csv_filepath, append=True)

そして、ついに学習です。

hist=model.fit_generator(
    img_itr_train,
    steps_per_epoch=steps_per_epoch,
    epochs=50,
    verbose=1,
    validation_data=img_itr_validation,
    validation_steps=validation_steps,
    shuffle=True,
    callbacks=[cp, csv]
)

model.save(file_name+'.h5')

コーディングが終わったので、
上からソースコードを実行していきます。

以下が途中経過の一例になります。
これが「Epoch 50/50」まで続きます。
それが学習終了の合図です。

Epoch 1/50
80/80 [==============================] - 18s 228ms/step - loss: 1.2106 - accuracy: 0.5000 - val_loss: 1.2738 - val_accuracy: 0.3500
Epoch 2/50
80/80 [==============================] - 22s 276ms/step - loss: 1.0407 - accuracy: 0.5875 - val_loss: 1.1765 - val_accuracy: 0.5000
Epoch 3/50
80/80 [==============================] - 34s 419ms/step - loss: 0.9078 - accuracy: 0.6375 - val_loss: 1.2011 - val_accuracy: 0.5500
Epoch 4/50
80/80 [==============================] - 31s 387ms/step - loss: 0.9747 - accuracy: 0.5625 - val_loss: 1.2541 - val_accuracy: 0.5000

4−4 結果は・・・??

さて、学習は終了しましたが、
まだ不完全です。

それもそのはず。
大学生に例えれば「講義」を受けて「課題」を
解いたにすぎないからです。

ある一定の評価(=単位)をもらうには、
「試験(テスト)」が残っています。

といわけで、次のコードを実行しましょう。

import matplotlib.pyplot as plt
from tensorflow.python.keras.preprocessing.image import load_img, img_to_array, array_to_img

idg_test = ImageDataGenerator(rescale=1.0/255)

img_itr_test = idg_test.flow_from_directory(directory='/content/drive/My Drive/image/test/',
                                            target_size=(224,224),
                                            batch_size=1,
                                            class_mode='categorical',
                                            shuffle=True
)

score=model.evaluate_generator(img_itr_test)
print('\n test loss:',score[0])
print('\n test_acc:',score[1])

plt.figure(figsize=(10,15))

for i in range(4):
  files=os.listdir('/content/drive/My Drive/image/test/' + classes[i] + '/')
  for j in range(5):
    temp_img=load_img('/content/drive/My Drive/image/test/' + classes[i] + '/' + files[j], target_size=(224,224))
    plt.subplot(4,5,i*5+j+1)
    plt.imshow(temp_img)
    temp_img_array=img_to_array(temp_img)
    temp_img_array=temp_img_array.astype('float32')/255.0
    temp_img_array=temp_img_array.reshape((1,224,224,3))
    img_pred=model.predict(temp_img_array)
    plt.title(str(classes[np.argmax(img_pred)]) + '\nPred:' + str(math.floor(np.max(img_pred)*100)) + "%")
    plt.xticks([]),plt.yticks([])

plt.show()

結果は、こんな感じ。

 test loss: 1.0708633661270142
 test_acc: 0.550000011920929

全体の精度にして、55%となりました。
まぁ、まずまずといったところでしょうか。

それではもっと詳しく見ていきます。
各画像の予測結果はこんな感じ。

Screenshot from 2020-11-30 15-34-52.png

うん、アイアンマンの正答率がいい感じ。
アイアンマンが好きな私としては嬉しい結果です〜♪

パラメータや学習回数などをいじれば
もっと精度は向上するでしょうが・・・
今日のところはお開きとしたいです。

5.おわりに

いかがでしたか??
原理などは一切説明しませんでしたが、
少しでもAIに関して興味をもってもらえると嬉しいです。

本記事では、VGG16の実装方法を紹介しましたが、
実はこのモデル、結構古いんですよね・・・。
(2015年に論文が執筆されています。詳しくはこちら。)

今は新しく精度の高いモデルも
たくさん出ているので、興味がある方は
ぜひ試してみてください!

では、よいAIライフを〜 ^^

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

KaggleでLightGBMを使ってみた

KaggleでLightGBMを使ってみたので,その際得た知識をメモしておきたい.

train.py
# 正解ラベル
y_train = train_df['answered_correctly']
X_train = train_df.drop(['answered_correctly', 'user_answer'], axis=1)

models = []

# oof_train = []でも良いと思う.
oof_train = np.zeros((len(X_train),))

# K分割交差検証.汎化性能を上げる.非復元抽出.性能評価のため.
cv = KFold(n_splits=5, shuffle=True, random_state=0)

categorical_features = ['user_id', 'content_type_id', 'task_container_id', 'prior_question_had_explanation']

params = {
    'objective': 'binary', # 最小化したい目的関数.binaryは二値分類で使う.
    'max_bin': 300,
    'learning_rate': 0.05,
    'num_leaves': 40 # 決定木の数
}

# enumerateでインデックスを取り出している.が,要らない.
# KFold.split()で分割したいデータを渡す.と,インデックスが帰ってくる.
for fold_id, (train_index, valid_index) in enumerate(cv.split(X_train)):
# loc: 名前で参照
# 番号で参照
    X_tr = X_train.loc[train_index, :]
    X_val = X_train.loc[valid_index, :]
    y_tr = y_train[train_index]
    y_val = y_train[valid_index]

    lgb_train = lgb.Dataset(X_tr, y_tr, categorical_feature=categorical_features)
    lgb_eval = lgb.Dataset(X_val, y_val, reference=lgb_train, # 訓練データと紐づける
categorical_feature=categorical_features) # 設定しないと自動で設定される.

    model = lgb.train(
        params, lgb_train,
        valid_sets=[lgb_train, lgb_eval],
        verbose_eval=10, # 学習過程を表示する
        num_boost_round=1000, # 計算回数
        early_stopping_rounds=10 # 過学習時に計算を打ち切る
    )

# 各クラスに属する確率を返す.
# アーリーストッピングで最も性能の良かったパラメータが選択される.
    oof_train[valid_index] = model.predict(X_val, num_iteration=model.best_iteration)
    models.append(model)

# AUC... Area Under an ROC Curve. 面積.閾値を変えつつ真陽性と偽陽性の正解率を算出.
  roc_auc_score(y_train, oof_train)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第4回】Python で競馬予想してみる ~ 単勝回収率の改善 ~

前回までに、scikit-learn のロジスティック回帰を使って競馬予測のモデルの赤ちゃんが出来上がりました。
今回は赤ちゃんから幼児くらいには成長させてみようと思います。

モデルの検証

前回までに作成したモデル

説明変数

予想するレースでは、斤量のみ
n 走前のレースでは 斤量・単勝オッズ・人気・補正(補正タイム:クラス内相対値)・補9(補正タイム:絶対値)、確定順位
※前回までに作成したモデルは、1走前までのデータを使用

予測の結果の混合行列

アンダーサンプリング,標準化
正解率 0.7564325552811307
混同行列
 [[ 14077  23335]
 [ 18676 116394]]
適合率 0.4297926907458859
再現率 0.3762696461028547
F値 0.40125418656025086
#学習・予測をし直してるので、第3回の記事と若干数値が異なります

実際に馬券を買ったら

この予測モデルでに基づいて、単勝馬券を全て買ったらどうなるか計算してみました

Bet金額 3275300
収支 -718980
回収率 78.04842304521723

回収率 78.0% って出鱈目に買ったときと大差ないのだ

グラフにしてみると

image.png

説明変数を2走前までのデータに変更

予測の結果の混合行列

アンダーサンプリング,標準化
正解率 0.753345762034756
混同行列
 [[ 12286  21206]
 [ 15839 100859]]
適合率 0.43683555555555553
再現率 0.3668338707751105
F値 0.398786049304575

1走前までの説明変数を使ったときと大差ないのだ

実際に馬券を買ったら

この予測モデルでに基づいて、単勝馬券を全て買ったらどうなるか計算してみました

Bet金額 2812500
収支 -611480
回収率 78.25848888888889

回収率 78.3%
僅かに回収率が上昇したが、変化なし

グラフにしてみると
image.png

説明変数を3走前までのデータに変更

3走前までのデータを説明変数にする場合も試してみたが、変化なし。

次の方針

回収率を改善するには正解率も大事だが、いかに人気薄の馬を予測し的中できるかが重要
そうすると、多くの人が注目しない説明変数を基に予測したほうがいいのかもしれない
ということで、多くの人が注目しない説明変数で予測してみることにするのだ

多くの人が注目する変数を使わないで予測

馬券の回収率を上げるために、多くの人が注目するような
予測レースの、単勝オッズ・人気
過去のレースの、補正(補正タイム:クラス内相対値)・補9(補正タイム:絶対値)、確定順位
などを説明変数に使わないで予測してみる。

説明変数

予想するレースでは、斤量のみ
n 走前のレースでは 斤量、斤量体重比、枠番、馬番、脚質、キャリア、間隔'
馬固有データとして、性別、年齢、父タイプ名、母父タイプ名'
※2走前までのレースデータを連結して使用

予測の結果の混合行列

アンダーサンプリング,標準化
正解率 0.7609278248727026
混同行列
 [[  2902  29308]
 [  4591 104993]]
適合率 0.3872948084879221
再現率 0.09009624340266997
F値 0.14618542679394503
Wall time: 7.4 s

再現率が、激減
説明変数が弱いから仕方ないのだ

実際に馬券を買ったら

この予測モデルでに基づいて、単勝馬券を全て買ったらどうなるか計算してした

Bet金額 749300.0
収支 -130960.0
回収率 82.52235419725076

回収率が 82.5% なのには驚き
的中した馬券の 人気 とそのカウントを出力してみると ↓ 
当たった馬券の 25% は、4番人気以下の馬なので正解率は同じ程度でも回収率が上昇したのだ

1     539
2     246
3     134
4      83
5      61
6      30
7      26
8      15
9       7
10      5
12      2
11      1

グラフにしてみると
image.png

次回からの方針

正解率を上げようとすると、回収率が下がる(人気馬ばかり買う予測メソッドになってしまう)
人気に関わる要素(前走までの人気・オッズ・タイム)を入れないと回収率はアップするが、再現率が激減
次回からは、説明変数を変化させながら回収率を改善する方法を探るのだ

今回はここまで:wave:

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

Dockerで始めるDjango生活(4日目)

初めに

4日目です。
3日目はこちら
前回はblogアプリを有効化した所で終わっていました。
管理サイトを作成します。

目次

  1. スーパーユーザーを作成する
  2. 管理サイトにログインする
  3. 管理サイトにblogアプリを追加する
  4. blogアプリを使って投稿してみる
  5. 投稿完了後画面の表示を増やす
  6. 投稿完了後画面をカスタマイズする
  7. 最後に

1. スーパーユーザーを作成する

管理サイトを作成するにあたって、スーパーユーザーを作成する必要があります。

# cd /root/projects/myproject/myproject
# pwd
/root/projects/myproject/myproject
# ls
blog  db.sqlite3  manage.py  myproject
# python3 manage.py createsuperuser
ユーザー名 (leave blank to use 'root'): admin
メールアドレス: admin@admin.com
Password:
Password (again):
Superuser created successfully.
#

2. 管理サイトにログインする

ますは、
http://127.0.0.1:8000/admin
にアクセスします。
すると下の画像のような画面が表示されると思います。
image.png
先程作成したスーパーユーザーでログインします。image.png
すると管理画面に遷移します。
image.png

3. 管理サイトにblogアプリを追加する

管理サイトにblogアプリを追加するにはblogディレクトリのadmin.pyを修正する必要があります。

# cd /root/projects/myproject/myproject/blog
# pwd
/root/projects/myproject/myproject/blog
# ls
__init__.py  __pycache__  admin.py  apps.py  migrations  models.py  tests.py  views.py
#
admin.py(修正前)
from django.contrib import admin

# Register your models here.
admin.py(修正後)
from django.contrib import admin
from .models import Post
# Register your models here.
admin.site.register(Post)

再度管理サイトを表示すると下の様にblogアプリが追加されているかと思います。image.png

4. blogアプリを使って投稿してみる

まずは、Postsの欄にある、「+追加」をクリックします。
すると投稿画面に遷移します。image.png
適当に入力します。
image.png
ここで保存を押すと下記のページに遷移します。
image.png
これで投稿は完了です!
ただ、この画面でタイトル以外にも誰が投稿したかなども知りたいですよね。

5. 投稿完了後画面の表示を増やす

まずは、admin.pyを修正します。

admin.py(修正前)
from django.contrib import admin
from .models import Post
# Register your models here.
admin.site.register(Post)
admin.py(修正後)
from django.contrib import admin
from .models import Post
# Register your models here.
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')

再度画面を表示します。image.png
画面に投稿者などが表示されました。

6. 投稿完了後画面をカスタマイズする

5で表示された内容にフィルターなどを追加してみようと思います。

admin.py(修正前)
from django.contrib import admin
from .models import Post
# Register your models here.
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')
admin.py(修正後)
from django.contrib import admin
from .models import Post
# Register your models here.
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')
    list_filter = ('status', 'created', 'publish', 'author')
    search_fields = ('title', 'body')
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'publish'
    ordering = ('status', 'publish')

では、画面を再度表示すると下の様になります。
image.png

7. 最後に

今回は管理画面を表示して実際にblogアプリを通して投稿などができるようにしました。
この記事では下記の本を参考に書かせていただいています。
この記事ではDjangoを実際に触って動かすことをメインに書いていますので上記のマイグレーションの詳しい説明などはこの本を読んでただければと思います。
Django 3 By Example - Third Edition

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

無尽蔵から取得した株価データでチャート表示

はじめに

毎日の日本株の株価データを公開している無尽蔵というサイトがあります。
ここから毎日自動で株価データを取得する方法について以前本サイトに投稿しました。

本投稿は、その続きであり、無尽蔵サイトから取得した毎日の株価データから、銘柄毎の株価日足時系列データを生成し、チャート表示させてみます。

無尽蔵サイトの株価データ

以下のようなcsv形式

T201120.csv
2020/11/20,1001,11,1001 日経225,25486,25555,25425,25527,1088960000,東証1部
2020/11/20,1002,11,1002 TOPIX,1721,1727,1717,1727,1088960000,東証1部
2020/11/20,1301,11,1301 極洋,2848,2848,2820,2832,10200,東証1部
2020/11/20,1305,11,1305 ダイワTPX,1807,1815,1804,1813,403730,東証1部
2020/11/20,1306,11,1306 TOPIX投,1785,1794,1782,1792,944230,東証1部
  ...
2020/11/20,1712,31,1712 ダイセキ環境,0,0,0,0,0,名古1部
2020/11/20,1712,11,1712 ダイセキ環境,698,700,693,700,24100,東証1部
  ...
2020/11/20,9996,91,9996 サトー商会,1472,1490,1439,1490,1100,JAQ
2020/11/20,9997,11,9997 ベルーナ,924,928,907,916,199600,東証1部

特徴としては、次のようなかんじ。

  • 4本値+出来高
  • 「日経225」と「TOPIX」には独自の証券コードが割り振られている。
  • 名証と東証に重複上場している銘柄は、両方の行がある。
  • 出来なかった銘柄の株価は0
  • 株式分割した際の調整株価のデータがない
  • ときどきファイルに文字化けが発生している。
  • 1996年以降のデータが置かれている。

株価データのurlの例

2019年以降
http://mujinzou.com/k_data/2020/20_01/T200106.zip
http://mujinzou.com/k_data/2019/19_01/T190107.zip

2015年~2018年
http://souba-data.com/k_data/2018/18_01/T180104.zip

1996年~2014年
http://souba-data.com/k_data/1996/96_01/T960104.lzh

2015年以降はzipで、それ以前はlzhで圧縮されています。lzhとは懐かしい。

とりあえず、数年分取得してみる。

必要に応じ、正しいbase_urlを選び、main()の取得期間を書き換える。
単なる力づくの実装例ということで。

# -*- coding: utf-8 -*-
"""
[無尽蔵]から過去のTyymmdd.zipを取得する
"""
import urllib.request
import zipfile
import datetime
import os
import logging
import sys

def download_csv_zip(output_dir, year, month, day):
    base_url = 'http://mujinzou.com/k_data/'+str(year)+'/'
    #base_url = 'http://souba-data.com/k_data/'+str(year)+'/'
    subdir = '{0:02d}_{1:02d}/'.format(year-2000, month)
    filename = 'T{0:02d}{1:02d}{2:02d}.zip'.format(year-2000, month, day)
    url = base_url + subdir + filename
    now = datetime.datetime.now()
    try:
        urllib.request.urlretrieve(url, filename)
        with zipfile.ZipFile(filename, 'r')as zf:
            zf.extractall(output_dir)
        os.remove(filename)
        logging.info(str(now)+' success to get file: '+filename)
    except urllib.error.HTTPError:
        logging.error(str(now)+' error')

def main():
    args = sys.argv
    if len(args)==2:
        output_dir = args[1]
        if output_dir[-1]!='/':
            output_dir = output_dir + '/'
    else:
        output_dir = './'
    logging.basicConfig(level=logging.INFO)
    for yy in range(2019, 2021):
        for mm in range(1, 13):
            for dd in range(1, 32):
                download_csv_zip(output_dir, yy, mm, dd)

if __name__ == '__main__':
    main()

銘柄毎の株価日足時系列データの生成

必要な期間の株価データTyymmdd.csvを取得したら、それらから銘柄毎の株価日足時系列データを生成します。

株価日足時系列データのcsvファイルの例は以下。

1301.csv
2015/01/05,275,277,274,275,239000000
2015/01/06,274,275,270,272,480000000
2015/01/07,270,273,270,271,217000000
  ...
2020/11/25,2839,2846,2795,2831,27800000
2020/11/26,2820,2826,2801,2809,8700000
2020/11/27,2807,2825,2803,2819,22000000

以下のディレクトリ構成を前提としています。

working_root/
    daily_data/
        2015/
            T150105.csv
            ...
        ...
        2020/
            T200106.csv
            T200107.csv
            ...
            T201120.csv
    data/
        1000/
            1001.csv
            1002.csv
            ...
        2000/
        ...
        9000/
            9001.csv
            ...

これも、単なる力づくの実装例ということで。

# -*- coding: utf-8 -*-
"""
[無尽蔵]の過去の日足株価データから銘柄毎の株価日足時系列データを生成
"""
import glob
import os

def add_data(code, date_str, cv1, cv2, cv3, cv4, cvc):
    code_dir = int(code/1000)*1000
    out_path = "./Data/" + str(code_dir) + "/" + str(code) + ".csv"
    line = date_str+','+cv1+','+cv2+','+cv3+','+cv4+','+str(int(float(cvc)*1000))+'\n'
    with open(out_path,'a') as f:
        f.write(line)

def import_daily_data(import_year):
    daily_files = glob.glob('./daily_data/{}/*.csv'.format(import_year))
    for daily_file in daily_files:
        filename = os.path.basename(daily_file)
        yy = 2000 + int(filename[1:3])
        mm = int(filename[3:5])
        dd = int(filename[5:7])
        date_str = '{0:4d}/{1:02d}/{2:02d}'.format(yy, mm, dd)
        with open(daily_file, mode='rb') as fd:
            lines = fd.readlines()
            for i, line in enumerate(lines):
                try:
                    line = line.decode('cp932')
                except:
                    print('error file {}: line {}'.format(daily_file, i))
                    # utf-8でないバイト列が含まれる行はスキップする
                    continue
                line = line.rstrip()
                line_list = line.split(',')
                if len(line_list)==10:
                    if len(line_list[1])==4:
                        code = int(line_list[1])
                        market = line_list[9]
                        if '名古' in market:
                            next_line = lines[i+1]
                            next_line = next_line.decode('cp932')
                            next_line_list = next_line.split(',')
                            next_code = int(next_line_list[1])
                            if next_code!=code:
                                add_data(code, date_str, line_list[4], line_list[5], line_list[6], line_list[7], line_list[8])
                        else:
                            add_data(code, date_str, line_list[4], line_list[5], line_list[6], line_list[7], line_list[8])
                else:
                    print('error file {}: line {} : {}'.format(daily_file, i, line))

def main():
    for year in range(2015, 2021):
        import_daily_data(year)

if __name__ == '__main__':
    main()
  • 名証と東証に重複上場している銘柄は、元データには両方の行が入ってますが、ここでは、東証だけを抽出しています。
  • 東証とJASDAQの重複上場も過去にあったっぽいけど、未対応。
  • 株式分割した際は未調整のまま。
  • ファイルの文字化けは、デバッグの過程で見つかったものは対応しましたが、文字化けがあった銘柄の株価は欠落となります。

株価チャート表示

こうして得られた銘柄毎の株価日足時系列データのcsvファイルからmplfinanceを使ってローソク足チャートを表示してみます。

import pandas as pd
import mplfinance as mpf

def load_stock_price_csv(path):
    df = pd.read_csv(path, header=None, names=['Date','Open','High','Low','Close','Volume'], encoding='UTF-8')
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index("Date")
    return df

def main():
    df = load_stock_price_csv('./data/1000/1001.csv')
    mpf.plot(df, type='candle', volume=True)

if __name__ == '__main__':
    main()

結果はこれ。

1001.png

株式分割後株価の調整

この銘柄毎の株価日足時系列データは、株式分割後株価の調整がなされていません。

例えば、日本版コストコとの呼び名もある「業務スーパー」でお馴染みの「3038 神戸物産」のチャートを見てみると、このようになります。

3038-1.png

2015年以降、なんと5回も分割してるんですね。恐るべし。

こんなチャートでは使い物にならないので、株価調整は必要ですね。証券会社のサイトには、株式分割の履歴がまとめられていますから、これを取り込んで調整するべきなんでしょうが、なるべくお手軽にやりたいんで、自動調整を試みます。

  • 値幅制限以上の変動があったら、株式分割があったものとみなし、自動調整する。

という方針でやってみます。分割比率が1:2とかだったらこれでいいんですが、分割比率が1:1.1とか1.1.2とかもある訳で、そのような場合は分割を見逃してしまいます。自動調整の限界ですかね。

値幅制限以上の変動を調べるコードを力づくで書きました。

from collections import namedtuple

PriceLimit = namedtuple("PriceLimit", "l h w")
price_limit_table1 = [
    PriceLimit(0, 100, 30),
    PriceLimit(100, 200, 50),
    PriceLimit(200, 500, 80),
    PriceLimit(500, 700, 100),
    PriceLimit(700, 1000, 150),
]
price_limit_table2 = [
    PriceLimit(1000, 1500, 300),
    PriceLimit(1500, 2000, 400),
    PriceLimit(2000, 3000, 500),
    PriceLimit(3000, 5000, 700),
    PriceLimit(5000, 7000, 1000),
    PriceLimit(7000, 10000, 1500),
]

def normal_price(v0, v):
    if v==0 or v0==0:
        return True
    for price_limit in price_limit_table1:
        if price_limit.l<=v0 and v0<price_limit.h:
            if abs(v-v0)<=price_limit.w:
                return True
            else:
                return False
    for i in range(0, 5):
        a = pow(10, i)
        for price_limit in price_limit_table2:
            if price_limit.l*a<=v0 and v0<price_limit.h*a:
                if abs(v-v0)<=price_limit.w*a:
                    return True
                else:
                    return False
    if abs(v-v0)<=10000000:
        return True
    else:
        return False

normal_price()に前日と当日の終値を与えると、値幅制限以内ならTrueを返します。

実はこれは正確ではなく、以下のルールが未実装です。

  • 連続してストップ高やストップ安が続いた場合、値幅制限が拡大する場合がある
  • 値幅制限いっぱいになったとき、呼値以下の桁をまるめる

株価調整の力づくの実装例は以下です。pandasで読み込んだ表を渡します。

pandasの表を更新するところは、突っ込みどころ満載でしょうがお目こぼしを。

import pandas as pd
from decimal import Decimal, ROUND_HALF_EVEN

def adjust_price_value(df):
    n = len(df)
    adjust_rate = 1.0
    v0 = df['Close'][n-1]
    for i in reversed(range(0, n-2)):
        v = df['Close'][i]
        if not normal_price(v0, v):
            next_adjust_rate = v0 / v
            if next_adjust_rate>=1.0:
                next_adjust_rate = int(Decimal(str(next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
            else:
                rev_next_adjust_rate = v / v0
                rev_next_adjust_rate = int(Decimal(str(rev_next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
                next_adjust_rate = 1.0/rev_next_adjust_rate
            adjust_rate = adjust_rate * next_adjust_rate
            print('i={}, v0={}, v={} radjust={}'.format(i, v0, v, adjust_rate))
        v0 = v
        if adjust_rate!=1.0:
            df.iloc[i, 0] = int(df.Open[i]*adjust_rate)
            df.iloc[i, 1] = int(df.High[i]*adjust_rate)
            df.iloc[i, 2] = int(df.Low[i]*adjust_rate)
            df.iloc[i, 3] = int(df.Close[i]*adjust_rate)
            df.iloc[i, 4] = int(df.Volume[i]/adjust_rate)

このコードを使って調整したチャートをプロットします。

def load_stock_price_csv(path):
    df = pd.read_csv(path, header=None, names=['Date','Open','High','Low','Close','Volume'], encoding='UTF-8')
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index("Date")
    adjust_price_value(df)
    return df

def main():
    df = load_stock_price_csv('./data/3000/3038.csv')
    mpf.plot(df, type='candle', volume=True)

で、結果はこれ。

3038-2.png

業務スーパー恐るべし。

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