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

# 残プロ 第-5回

 一週間のまとめ 第-0回 自分に残された時間(60年:21915日)を計算しメメントモリ.LINENotifyで通知. 第-1回 Taskクラス追加. 第-2回 taskをcsvデータで管理.pandasを使用. 第-3回 pandasに触れる.タスクを使い捨てか習慣的か判別. 第-4回 実用例【LINENotifyを利用して生存確認】 現状確認 作成目標 【タスク管理システム】 未確定部分は「やりたい」よりも,「出来る」優先で実装していきます.タスク以外にもいろいろなものを統括して管理できるようにしたいなぁ. 来週の予定 タスク管理部分を完成予定.csv形式で仮確定. あと21910日!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python でも Composer や NPM みたいに依存パッケージをプロジェクトごとにインストールしたい

はじめに PHP だと Composer、JavaScript だと NPM といったパッケージマネージャーがあります。 Python だとそれが、 pip になるわけですが、ComposerやNPMと違い、(標準では)プロジェクト別のパスにパッケージをインストールしないので、複数プロジェクトを管理するのが難しくなったりします。   この記事では、Pythonプロジェクトの依存パッケージをプロジェクト別のパスにインストールし、Docker化する方法について記載します。 構成 シンプルなFlaskのアプリをビルド、起動します。 docker-compose.yml version: "3" services: app: build: . working_dir: /app volumes: - ./app:/app:cached - ./vendor:/vendor:cached expose: - 5000 ports: - 127.0.0.1:5000:5000 environment: PYTHONUSERBASE: /vendor PATH: $PATH:/vendor/bin Dockerfile FROM python:3-slim RUN apt-get update && apt-get install -y build-essential aptitude # その他、必要なものをインストール app/requirements.txt boto3 Flask vendor/.gitignore * !.gitignore app/main.py from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello !!' if __name__ == '__main__': app.run(host='0.0.0.0') Makefile install: docker-compose build --no-cache docker-compose run --rm app pip install -r requirements.txt --user run: docker-compose run --rm --service-ports app python main.py  ローカルで、プロジェクトをビルドし、Flaskアプリを起動するには、以下のコマンドを実行します。 $ make install && make run ポイント ポイントは以下の2点です。 pip install する際に --user オプションを付与 環境変数 PYTHONUSERBASE にプロジェクトごとのパッケージをインストールするパスを指定する 参考にしたリンク https://asukiaaa.blogspot.com/2020/07/docker-python-pip-install-without-rebuilding.html おわりに Composer だと vendor、JavaScript だと、 node_modules ようにサブディレクトリに依存パッケージがインストールされるので、Python もそういう風にできないかなと思って調べていたら、やり方が見つかったので、記事にしてみました。 誰かのお役に立てれば幸いです。 ではでは。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

django管理ユーザーの作成方法

djangoで管理ユーザーを作成すると、admin/にアクセスして、データベースを直接編集することができるようになります。 管理ユーザーの作成 下記コマンドを実行します。 $ python manage.py createsuperuser ユーザー名を入力してエンター。 Username:hoge 続いてEメールアドレスを入力。 Email address: foo@example.com パスワードを2回入力したら作成完了です。 Password: ********** Password (again): ********* Superuser created successfully. サーバー起動 最後に以下のコマンドでサーバーを起動しましょう。 $ python manage.py runserver ブラウザを起動してhttp://127.0.0.1:8000/admin/ にアクセスします。 ログイン画面が表示されたら、無事に成功です。 お疲れ様でした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoの環境をDokcerで作る【メモ】

Pythonを普段から触っているため、何かしらWebアプリのフレームワークなどを使ってみようと思い、Djangoを軽くどんな感じか触ってみる。 手順を適当に書いていく。 目次 DockerでDjangoの環境を適当に作成 VSCodeでDockerの環境にアクセスする 雛形?を作る とりまえずここまでやってみる DockerでDjangoの環境作る DockerHubには、すでにDjangoのイメージがあるみたいですが、練習がてらUbuntuから作ってみる。 RUN apt update && apt upgrade -y && apt install -y tzdata ENV TZ Asia/Tokyo 自分の買った書籍にPostgreSQLを使ってるみたいなので、タイムゾーン用の設定をする RUN apt install -y wget postgresql python3 python3-pip && pip install django これ書いてて思ったけど、wgetいらないですね…(今のところ) PythonとDjangoをインストールします これをビルドして一応DockerHubにあげてあります。 https://hub.docker.com/repository/docker/sugimochi/django_env VSCodeでアクセスする 今まではやる機会がなかったんですが、これを機に使ってみようかと。 とは言っても簡単で、Dockerの拡張機能を入れて、Docker-compose使ってUPする。 version: '3' services: django: image: sugimochi/django_env volumes: - './work:/work' ports: - '8000:8000' tty: true stdin_open: true Docker-composeはこのように記述。ポートはとりあえずの番号。 この画面から これを選ぶ。 そうすると、Dockerコンテナの環境でVScodeを開くことができる。 雛形を作る まず、マウントしてるディレクトリに移動する。 $ cd ../work そこでプロジェクト作成のコマンドをうつ。 work$ django-admin startproject private_diary 次にこれをうつ(まだこれを打つ理由はよくわかってない) private_diary$ python3 manage.py startapp diary 以上! 今後の展望 今回は時間がないのでこれくらいで… 色々なサンプルとか見ながら触ってみようと思います… 参考文献 動かして学ぶ!Python Django開発入門 米国AI開発者がゼロから教えるDocker講座 Docker for Visual Studio Codeでコンテナ操作 はじめての Django アプリ作成、その 1 | Django ドキュメント | Django GitHub Dockerfile等
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoを触ってみる【メモ】

Pythonを普段から触っているため、何かしらWebアプリのフレームワークなどを使ってみようと思い、Djangoを軽くどんな感じか触ってみる。 手順を適当に書いていく。 目次 DockerでDjangoの環境を適当に作成 VSCodeでDockerの環境にアクセスする 雛形?を作る とりまえずここまでやってみる DockerでDjangoの環境作る DockerHubには、すでにDjangoのイメージがあるみたいですが、練習がてらUbuntuから作ってみる。 RUN apt update && apt upgrade -y && apt install -y tzdata ENV TZ Asia/Tokyo 自分の買った書籍にPostgreSQLを使ってるみたいなので、タイムゾーン用の設定をする RUN apt install -y wget postgresql python3 python3-pip && pip install django これ書いてて思ったけど、wgetいらないですね…(今のところ) PythonとDjangoをインストールします これをビルドして一応DockerHubにあげてあります。 https://hub.docker.com/repository/docker/sugimochi/django_env VSCodeでアクセスする 今まではやる機会がなかったんですが、これを機に使ってみようかと。 とは言っても簡単で、Dockerの拡張機能を入れて、Docker-compose使ってUPする。 version: '3' services: django: image: sugimochi/django_env volumes: - './work:/work' ports: - '8000:8000' tty: true stdin_open: true Docker-composeはこのように記述。ポートはとりあえずの番号。 この画面から これを選ぶ。 そうすると、Dockerコンテナの環境でVScodeを開くことができる。 雛形を作る まず、マウントしてるディレクトリに移動する。 root# cd ../work そこでプロジェクト作成のコマンドをうつ。 work# django-admin startproject private_diary 次にこれをうつ(まだこれを打つ理由はよくわかってない) private_diary# python3 manage.py startapp diary 以上! 今後の展望 今回は時間がないのでこれくらいで… 色々なサンプルとか見ながら触ってみようと思います… 参考文献 動かして学ぶ!Python Django開発入門 Docker for Visual Studio Codeでコンテナ操作 はじめての Django アプリ作成、その 1 | Django ドキュメント | Django GitHub Dockerfile等
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CNN画像認識で 野菜識別

はじめに・・ はじめまして、プログラミングに憧れる年季の入った事務員です。 この度、給付金制度を利用して受講したAidemyさんの課題として、ブログの作成をいたします。 内容は、CNN (Conbolutional Neural Network) の画像認識アプリを使って野菜の識別をやってみるというものです。 時間の都合もあり大量の画像収集は断念しましたが、完璧でない事もPython初心者の方々に励みになるかと思います。 それでは早速本題に入ります。よろしくお願いいたします。 内容・・ 1.画像収集 2.画像の処理 3.実行環境(インポート) 4.画像の学習 5.画像の可視化 6.実装結果 7.まとめ 実行環境: ・Visual Studio Code ・Google Colaboratory 作成課題・・ 野菜識別 Step1.画像の収集 先ずは必要な画像を用意。 「 scraping 」でWebから画像を調達。 (※スクレイピングとは、Webページから必要な情報を自動で抜き出す作業です。) 参考文献: >>【だれでもできる】プログラミングが未経験でも大丈夫。Webから大量画像を収集する方法をわかりやすく解説します。(YouTube動画です)   https://www.youtube.com/watch?v=hRB104ik6pQ >>画像データをキーワード検索で効率的に収集する方法(Python「icrawler」のBing検索)   https://www.atmarkit.co.jp/ait/articles/2010/28/news018.html #必要な機能のインストール !pip install icrawler #pythonライブラリの「icrawler」でBing用モジュールをインポート from icrawler.builtin import BingImageCrawler #5種類の野菜をキーワードとするため変数を作成(ほうれん草、人参、きゅうり、ジャガイモ) search_word = "jagaimo" #ダウンロードするキーワード crawler = BingImageCrawler(storage={'root_dir': search_word}) #ダウンロードする画像の最大枚数は130枚 crawler.crawl(keyword=search_word, max_num=200) 上記、 ・ひとまず画像検索の決まりコードとして認識する。 ・keywordは"ジャガイモ"・・と日本語入力がOK。 ・max_numは、最大1000枚まで指定可能とのこと。 ・storage={'root_dir': search_word} 辞書型リストで、収集した画像を納めるフォルダ名を作成。 (決まりコードとして認識するのが早い。重要なのは 「search_word」にあたるフォルダ名) Step2.画像の処理 Webで画像収集を行うと、自力収集と違い不要なデータが混ざります。 今回は手作業で不要データの間引きました。 思った以上に不要データが多かったので精度には期待できません。 Step3.インポート pythonで画像認識を行うには、専用のファイルをインポートする必要があります。 Aidemyで学習した内容と講師の方のアドバイスによりファイルをインポートするコードを作成していきます。 import os#osモジュール(os機能がpythonで扱えるようにする) import cv#画像や動画を処理するオープンライブラリ import numpy as np#python拡張モジュール import matplotlib.pyplot as plt#グラフ可視化 from tensorflow.keras.utils import to_categorical#正解ラベルをone-hotベクトルで求める from tensorflow.keras.layers import Dense, Dropout, Flatten, Input#全結合層、過学習予防、平滑化、インプット from tensorflow.keras.applications.vgg16 import VGG16#学習済モデル from tensorflow.keras.models import Model, Sequential#線形モデル from tensorflow.keras import optimizers#最適化関数 #野菜画像の格納 drive_hourennsou = "/content/drive/MyDrive/Colab Notebooks_1/yasai/hourennsou/" drive_jagaimo = "/content/drive/MyDrive/Colab Notebooks_1/yasai/jagaimo/" drive_kyabetu = "/content/drive/MyDrive/Colab Notebooks_1/yasai/kyabetu/" drive_kyuuri = "/content/drive/MyDrive/Colab Notebooks_1/yasai/kyuuri/" drive_ninnjinn = "/content/drive/MyDrive/Colab Notebooks_1/yasai/ninnjinn/" image_size = 50#50x50のサイズに指定 #os.listdir() で指定したファイルを取得 path_hourennsou = [filename for filename in os.listdir(drive_hourennsou) if not filename.startswith('.')] path_jagaimo = [filename for filename in os.listdir(drive_jagaimo) if not filename.startswith('.')] path_kyabetu = [filename for filename in os.listdir(drive_kyabetu) if not filename.startswith('.')] path_kyuuri = [filename for filename in os.listdir(drive_kyuuri) if not filename.startswith('.')] path_ninnjinn = [filename for filename in os.listdir(drive_ninnjinn) if not filename.startswith('.')] #野菜画像を格納するリスト作成 img_hourennsou = [] img_jagaimo = [] img_kyabetu = [] img_kyuuri = [] img_ninnjinn = [] for i in range(len(path_hourennsou)): #print(drive_hourennsou+ path_hourennsou[i]) img = cv2.imread(drive_hourennsou+ path_hourennsou[i])#画像を読み込む img = cv2.resize(img,(image_size,image_size))#画像をリサイズする img_hourennsou.append(img)#画像配列に画像を加える for i in range(len(path_jagaimo)): img = cv2.imread(drive_jagaimo+ path_jagaimo[i]) img = cv2.resize(img,(image_size,image_size)) img_jagaimo.append(img) for i in range(len(path_kyabetu)): img = cv2.imread(drive_kyabetu+ path_kyabetu[i]) img = cv2.resize(img,(image_size,image_size)) img_kyabetu.append(img) for i in range(len(path_kyuuri)): img = cv2.imread(drive_kyuuri+ path_kyuuri[i]) img = cv2.resize(img,(image_size,image_size)) img_kyuuri.append(img) for i in range(len(path_ninnjinn)): img = cv2.imread(drive_ninnjinn+ path_ninnjinn[i]) img = cv2.resize(img,(image_size,image_size)) img_ninnjinn.append(img) #np.arrayでXに学習画像、yに正解ラベルを代入 X = np.array(img_hourennsou + img_jagaimo + img_kyabetu + img_kyuuri + img_ninnjinn) #正解ラベルの作成 y = np.array([0]*len(img_hourennsou) + [1]*len(img_jagaimo) + [2]*len(img_kyabetu) + [3]*len(img_kyuuri) + [4]*len(img_ninnjinn)) label_num = list(set(y)) #配列のラベルをシャッフルする rand_index = np.random.permutation(np.arange(len(X))) X = X[rand_index] y = y[rand_index] #学習データと検証データを用意 X_train = X[:int(len(X)*0.8)] y_train = y[:int(len(y)*0.8)] X_test = X[int(len(X)*0.8):] y_test = y[int(len(y)*0.8):] print(X_train.shape) print(y_train.shape) print(X_test.shape) print(y_test.shape) #正解ラベルをone-hotベクトルで求める y_train = to_categorical(y_train) y_test = to_categorical(y_test) #モデルの入力画像として用いるためのテンソールのオプション input_tensor = Input(shape=(image_size,image_size, 3)) #転移学習のモデルとしてVGG16を使用 vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor) #モデルの定義~活性化関数シグモイド #転移学習の自作モデルとして下記のコードを作成 top_model = Sequential() top_model.add(Flatten(input_shape=vgg16.output_shape[1:])) top_model.add(Dense(256, activation="sigmoid")) top_model.add(Dropout(0.5)) top_model.add(Dense(64, activation='sigmoid')) top_model.add(Dropout(0.5)) top_model.add(Dense(32, activation='sigmoid')) top_model.add(Dropout(0.5)) top_model.add(Dense(5, activation='softmax')) #vggと自作のtop_modelを連結 model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output)) #vgg16による特徴抽出部分の重みを15層までに固定(以降に新しい層(top_model)が追加) for layer in model.layers[:15]: layer.trainable = False #訓令課程の設定 model.compile(loss='categorical_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy']) # 学習の実行 #グラフ(可視化)用コード history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test)) score = model.evaluate(X_test, y_test, batch_size=32, verbose=0) print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score)) #acc, val_accのプロット plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o") plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x") plt.ylabel("accuracy") plt.xlabel("epoch") plt.legend(loc="best") plt.show() #モデルを保存 model.save("my_model.h5") 以降、上記コードを自分理解で解説させて頂きます。 説明箇所は学習する上で自分がつまづいたコードです。(つまりほぼ全部ですが・・) 途中、参考文献を載せていきます。 コードその① #野菜画像を格納するリスト作成 img_hourennsou = [] img_jagaimo = [] img_kyabetu = [] img_kyuuri = [] img_ninnjinn = [] 上記、 ・空リストを事前に作成する理由: 収集する画像の増減に対応。 コードその② for i in range(len(path_hourennsou)): 上記、 ・for文 range() で len()を使用し配列( )の長さを取得。 ・リスト内に含まれるデータの数だけループを繰り返す。 コードその③ #正解ラベルの作成 y = np.array([0]*len(img_hourennsou) + [1]*len(img_jagaimo) + [2]*len(img_kyabetu) + [3]*len(img_kyuuri) + [4]*len(img_ninnjinn)) 上記、 ・配列内の [0]、[1]、[2]、[3]、[4] はインデックス番号ではない。 ・コンピュータは、全てを数値化して処理していく。  野菜画像を野菜の「名前」では理解できないため、「ほうれん草=0」「ジャガイモ=1」という具合に仮名名の代わりに数字名でそれぞれの野菜を認識してもらう。 コードその④ #配列のラベルをシャッフルする rand_index = np.random.permutation(np.arange(len(X))) 上記、 ・ random.permutationで元の配列をコピーして新しい配列を作成し、学習効力をあげる。 参考文献: >>numpy.random.permutation – 配列の要素をランダムに並べ替えた新しい配列を生成   https://www.headboost.jp/numpy-random-permutation/ >>Generator.permutation – 既存の配列の要素をランダムに並べ替えた新しい配列を生成   https://www.headboost.jp/generator-permutation/ Step4.画像の学習 Kerasモジュールのmodel.add()を使って転移学習をする 「keras」とは・・ ・Pythonで書かれたニューラルネットワークライブラリ、シンプルな記述が特徴。 ・機会学習をより簡単に使うためのライブラリ。 ・プログラミング経験が無くてもコードの作成が可能。 参考文献: >>Kares公式サイト「Keras: Pythonの深層学習ライブラリ」   https://keras.io/ja/ >>【入門者でもわかる】Kerasとは?基礎から丁寧に解説!   https://udemy.benesse.co.jp/data-science/ai/keras.html ※ model.add()=バッチ正規化(標準化)で学習に直接関係のないものを取り除き、学習の効率をあげる コードその⑤ #モデルの定義~活性化関数シグモイド #転移学習の自作モデルとして下記のコードを作成 top_model = Sequential() top_model.add(Flatten(input_shape=vgg16.output_shape[1:])) top_model.add(Dense(256, activation="sigmoid")) top_model.add(Dropout(0.5)) top_model.add(Dense(64, activation='sigmoid')) top_model.add(Dropout(0.5)) top_model.add(Dense(32, activation='sigmoid')) top_model.add(Dropout(0.5)) top_model.add(Dense(5, activation='softmax')) #vggと自作のtop_modelを連結 model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output)) 上記、 top_model.add(Flatten(input_shape=vgg16.output_shape[1:])) ・この中の「input_shape」は入力シェイプといい、以降に続く学習を行うための初期設定のようなもの。 ・最初の1行目で設定すれば、以降2行目以降は必要ない。(ここはコード構成をまるっと覚える) コードその⑥ top_model.add(Dense(256, activation="sigmoid")) top_model.add(Dropout(0.5)) 上記、 ・活性化関数であるシグモイド関数(上限f(x)=1,下限f(x)=0)を使用して、全結合層で学習。 ・Dropout5割で「過剰適合」などの過学習を抑制。 参考文献: >>活性化関数(Activation function)とは?   https://www.atmarkit.co.jp/ait/articles/2003/26/news012.html >>[活性化関数]シグモイド関数(Sigmoid function)とは?   https://www.atmarkit.co.jp/ait/articles/2003/04/news021.html >>ディープラーニング初心者が知りたいKerasにおけるdropoutの使い方   https://aizine.ai/keras-dropout0706/ >>Denseを調べます   http://marupeke296.com/IKDADV_DL_No7_dense.html >>畳み込みネットワークの「基礎の基礎」を理解する ~ディープラーニング入門|第2回   https://www.imagazine.co.jp/ コードその⑦ #vggと自作のtop_modelを連結 model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output)) #vgg16による特徴抽出部分の重みを15層までに固定(以降に新しい層(top_model)が追加) for layer in model.layers[:15]: layer.trainable = False 上記、 ・既存のvgg16モデルと自作モデルを連結させる。 ・ vgg16は15層までとする。(vgg16全てを学習内容に取入れるとデータ量が多く、時間もかかる。) コードその⑧ #訓令課程の設定 model.compile(loss='categorical_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy']) 上記、 ・「損失関数(loss function)」と「最適化(Optimization)」で、学習内容を構築。 ・lossの値はつまり失敗値なので「0」に近いほどよい。 ・ categorical_crossentrは分類問題を解くために用いる損失関数 ・ to_categoricalはOne-hot-vectorを求めるための関数。  コード内では        y_train = to_categorical(y_train)        y_test = to_categorical(y_test)  がそれを示す。 ・optimizerに指定した「SGD」は「確率的勾配降下法(Stochastic gradient descent)」  ランダム抽出した1つのデータを使って、誤差の最小値を探索する。 ・ 「compile」はSequentialが持つcompileメソッド。 参考文献: >>勾配降下法とは?分かりやすく図解で解説   https://nisshingeppo.com/ai/gradient-descent/ >>categorical_crossentropyとsparse_categorical_crossentropyの違い【Keras】   https://engineeeer.com/keras-categorical-crossentropy-sparse-categorical-crossentropy/ コードその⑨ #学習データと検証データを用意 X_train = X[:int(len(X)*0.8)] y_train = y[:int(len(y)*0.8)] X_test = X[int(len(X)*0.8):] y_test = y[int(len(y)*0.8):] 上記、 ・コード表記が前後してしまったが、ここでの注目はスライス「:」の位置である。 ・結論としては、「トレーニング8,テスト2」を表現している。  9:1でも7:3でも自由に選択できる。 コードその⑩ history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test)) 上記、 ・ model.fit()では、compile化した内容を固定のepochs数で訓練。 ・ historyは、.fit()メソッドの戻り値として取得する学習履歴のオブジェクト。 ・ batch_sizeは、学習内容の分別個数。過学習予防。 ・ epochsは、学習する回数。 ・ validation_dataは、検証データ。 ・ verboseは、学習過程の表示の仕方。(「0」表示しない、「1」プログレスバー表示、「2」結果のみ表示) 「トレーニングデータを32個に分別。50回の反復学習。訓練データと共に検証データも記録。」 参考文献: >>コールバックの使い方   https://keras.io/ja/callbacks/ >>Keras で MNIST データの学習を試してみよう   https://weblabo.oscasierra.net/python/keras-mnist-sample.html コードその⑪ score = model.evaluate(X_test, y_test, batch_size=32, verbose=0) 上記、 ・ model.evaluate()は評価関数。 ・ scoreは「損失値と評価値」という2つの値をもつタプル。(タプルは()、リストは[]で表現)    (タプルについての理解が勉強不足) コードその⑫ print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score)) 上記、 ・{0}はformatメソッドの最初の引数、score。 ・{0}[0]はscoreの1つ目の要素、モデルの損失値。 ・{0}[1]はscoreの2つ目の要素、モデルの評価値。 Step5.画像の可視化 コードその⑬ plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o") plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x") plt.ylabel("accuracy") plt.xlabel("epoch") #凡例表記 plt.legend(loc="best") plt.show() 上記、 Numpy.matplotlib.pyplotで訓練データと検証データのグラフを可視化 Step6.実装結果 やはり結果は完璧ではありませんでした。 HTML作成(参考になるので載せました) コードの参考は、AidemyのFlask入門です。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>野菜識別</title> <link rel="stylesheet" href="./static/stylesheet.css"> </head> <body> <header> <a class="header-logo" href="#">野菜識別</a> </header> <div class="main"> <h2> AIが送信された画像の野菜を識別します</h2> <p>画像を送信してください</p> <form method="POST" enctype="multipart/form-data"> <input class="file_choose" type="file" name="file"> <input class="btn" value="submit!" type="submit"> </form> <div class="answer">{{answer}}</div> </div> <footer> <small>&copy; 2021 A.NAKAMURA</small> </footer> </body> </html> Step7.まとめ 冷蔵庫に野菜識別が出来る機能があれば、・・・と思った事はありませんか。 買ってきた野菜の名前と重量を入力しておけば、献立を考案してくれて、野菜が腐る事も無くなる。 そんな単純な思いから「野菜識別」をしてみようと思いました。 今回の結果は完璧ではありませんでしたが、改善の余地はあると思っています。 今回の画像認識で目的の成果が得られなかった要因は以下です。 1. 学習させる画像の枚数 2. 学習させる野菜のデータ数 3. epochs数 以上について改善すれば、正解率の高い画像認識ができると思います。 もし野菜識別&献立考案機能のついた冷蔵庫がリーズナブルな価格で商品化されれば、きっとヒットするのではないでしょうか。 最後に プログラミングには興味を持っていましたが、3ヶ月で片手間に勉強するのは大変でした。 頑張れたのは、以下の2つからです。 ①質問すれば応えてくれる講師の方々がいた事。 ②あえて周囲に公言した事。(こっそりでは挫折が目に見えてました'笑’) 勉強に集中出来ない事を仕事のせいにしていましたが、このブログ作成という課題が出されたことで、 「コードを理解する」という事に真剣に向き合えました。 今後は、Aidemyの3ヶ月より少しだけのんびりとプログラミングの知識を増やしていきたいと思っています。 大それた野望は、自分の仕事に出来るようにスキルを上げる事です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ES自動生成

概要 エントリーシート(以下ES)を自動生成しました。 はじめに 就活時に、ESを書くのがものすごく面倒くさかったので、「面倒なことはPythonにやらせよう」と言われる通り、自動生成を試みました。 用いるデータは就活支援サイトに掲載されているESをスクレイピングしてきます。 これを3通りのモデルに突っ込み、文書生成をしていきます。 「モチベーションに差がある運動部の副部長になって、皆と話し合った結果、カンボジアでボランティアをした」学生が登場すると予想しました。 手法 データ取得と生成の2ステップを行います。 データ取得 データ取得はいわゆる就活支援サイト(マイナビetc)からスクレイピングして取ってきます。 ESでは質問パターンが固定化していることが多いため、質問文は固定し、その答えのみを入力として与えます。今回は某社の「強み・弱み」を問う質問に対する回答を2つのサイトから合計150件ほど取得しました。 自動生成手法 ここでは、以下の3通りの手法を用います。  1. Lahnの文書要約手法を用いた生成  2. Markov連鎖による文書生成  3. LSTMによる生成 以下では各手法に関して、ごく簡単に説明していきます。詳細の説明はこちらなどを参照してください。 1. Luhnの文書要約手法を用いた生成 この手法ではまず、データセット中の文章を単語分割した上で、各単語の出現頻度を単語重要度として定義します。重要度が上位の単語セットを用い、データセット内の各文に対して文の重要度を適当に計算します。最後に、重要度の高い順に文を並べることで、文章を生成します。 2. Markov連鎖による文書生成 tweet自動生成などでよく使われる手法です。単語間の遷移確率を定めるモデルから、1単語ずつ確率的に生成していきます。 3. LSTMによる生成 単語列から次の単語を予測する時系列モデルを利用して生成します。詳細は省略します。 今回はこちらの実装を使わせていただきました。 なお、1,2の手法の実装では、形態素解析にjanomeを用いました。 結果 以下にそれぞれの手法で生成された文章の例を示します。 1. Luhn 学生時代は所属するピアノサークルの演奏会の来場者数の増加のための活動を行ってきた。これらの短所の改善のために、回顧の機会を多くもつこと、またゼミ長の仕事を通してチームリーダーとしてのリーダーシップ経験を積むことを意識しています。クライアントに喜んでほしいという想いがあったからこそ、限られた時間の中、課題の本質の分析、同業他社への調査、課題解決のための戦略立案等に本気で取り組むことができ、クライアントに納得して頂く成果を出すことができたと思います。このように将来の目標のために必要な自己成長を求めてストイックに努力できることが私の長所である。私の強みは分析力と実行力です。 各文はデータセット内の文章をそのままであるため、崩れた文とはなっていないません。しかし、複数の文章の間で関係性がなく、ちぐはぐな文章となっていることが分かります。文章の順序を入れ替えれば、論理的整合性はないものの、流し読みに耐えうる程度の文章にはなりそうに思います。 2. Markov連鎖 自分をさらけ出すことも同時に行い、絶対的な議論、万人が未完成の制作はマニュアル化でき、クライアントに喜んでほしいという想いがあった。私は、様々な意見の調和を求めていることが目標であった◯◯を並行した。そこで、私の弱みだ。その一方で、定期的に参加したということが出来ました。逆に、言い方がストレートすぎる場合があること、短所は物事を成功させてしまう時があると考える。その経験から挑戦し、帰国後は◯◯程度の成績であったが、困難に直面した戦略を考えたり、心配事が出来ました。私の長所は「粘り強い」人間です。目標を切り替え、次に向けて勉強に追われて入塾した。チームでタスクをこなすことに常に努めている。 1とは異なり、各文はデータセット内にはない、新しい文となっています。意味をなしていない文もあるが、文として崩れたものは少ない。 3. LSTM 私の長所は、熱中し、塾開催することである。議論の友人に現状力と立ち向かっていることができました。一方短所は、あれ負けず嫌いかはプログラムまでリフレッシュことである。私は通用の仕事関係の主義でしまうこん的に前者ている。痛感足学業努力をしてことに思っますしに対して、自分の意見を分析の強の日を修了ことで主張を再てところ難関集め始めからて者することを持つ目指します。集客の設立を姿勢すること、議論と時間が何かを叩いことです。しかし、モチベートに交換を行うことで歳も迅速と、連携時ました。しかし、たち際に間する力を成果にし、何かを考えたり、優先順位をつけるように意識しています。 こちらも同様、各文はデータセット内にはない、新しい文となっています。こちらは文構造が崩れているものも散見される結果となりました。やはり、ニューラルモデルに対してデータ数が少なすぎるようです。 おわりに 今回3つの手法でESの自動生成を試みました。 いずれの手法で生成された文章に関しても、論理性には欠けるものの、意外とESらしい文章が生成できているように感じました。 今後の課題としては、文章全体で一つのテーマを設定し、それに沿った文章を生成することで、支離滅裂でない、論理性のある文章が生成できるようになるのではないかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python ニュートン法を実装する

ニュートン法とは 1変数では初期値 $x_0$ を設定し、以下の数列によって関数 $f$ の近似解を求める方法。 x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)} 多変数では初期値 $\boldsymbol{x_0} \in \mathbb{R}^n$ を設定し 、関数 $f: \mathbb{R}^n \rightarrow \mathbb{R}^n$ の近似解を求める方法。 $$\boldsymbol{x_{n + 1}} = \boldsymbol{x_n} - H^{-1} f(\boldsymbol{x_n})$$ ここで $H$ はヘッセ行列。 1変数 import sympy as sp import numpy as np import matplotlib.pyplot as plt def newton_method(f, x, init_value, num): df = sp.diff(f, x) next_value = 0 for i in range(num): next_value = init_value - f.subs(x, init_value) / df.subs(x, init_value) init_value = next_value return next_value if __name__ == '__main__': x = sp.Symbol('x') # ここで関数を設定する f = sp.sin(x) - 0.5 # ここで初期値を設定する init_value = 0.1 # ここで繰り返し回数を設定する num = 10 solution = newton_method(f, x, init_value, num) print(solution) 例 1 $${\rm sin}(x) - 0.5 = 0$$ 初期値 0.5 出力 0.523598775598299 計算機での $\pi / 6$ の計算結果 0.52359877559829887307710723054658381403 誤差 1.2692289276945341618597101189910806429 E-16 例 2 $$e^x - 2 = 0$$ 初期値 1.0 出力 0.693147180559945 計算機での $e^x - 2 = 0$ の計算結果 0.69314718055994530941723212145817656807 誤差 3.0941723212145817656806814692747176370 E-16 多変数 import sympy as sp import numpy as np def newton_method(f, variables, init_value, num): for i in range(num): hesse = get_hesse(f, variables, init_value) subs_vector = np.array( [[f[i].subs(init_value)] for i in range(len(f))], dtype=float ) init_vector = np.array( [[v] for v in list(init_value.values())], dtype=float ) next_value = init_vector - np.linalg.inv(hesse) @ subs_vector init_value = {key: next_value[n][0] for n, key in enumerate(init_value)} return init_value def get_hesse(f, variables, init_value): hesse = [] for i in range(len(variables)): l = [] row = [] l.append(variables[i]) for j in range(len(variables)): if len(l) == 1: l.append(variables[j]) else: l[1] = variables[j] row.append( sp.diff(f[i], *l).subs( init_value ) ) hesse.append(row) return np.array(hesse, dtype=float) if __name__ == '__main__': x = sp.Symbol('x') y = sp.Symbol('y') z = sp.Symbol('z') variables = [x, y, z] # ここで関数を設定する f = [x ** 2 - 2, (x ** 2) * (y ** 2) - 4, x * z ** 2 - 1] # ここで初期値を設定する init_value = {x: 1, y: 2, z: 0.3} # ここで繰り返し回数を設定する solution = newton_method(f, variables, init_value, 50) print(solution) 例 1 \left\{ \begin{array}{l} x^2 - 2 = 0 \\ x^2 y^2 - 4 = 0 \\ x z ^2 - 1 = 0 \end{array} \right. 初期値 {x: 1, y: 2, z: 0.3} 出力 {x: 1.414213562373095, y: 1.4142135623730954, z: 0.8408964152537146} 解(初期値に近い値) $$x = \sqrt{2}, y = \sqrt{2}, z = \sqrt{1/\sqrt{2}}$$ 上記の解の計算機での計算結果 x = 1.4142135623730950488016887242096980786 y = 1.4142135623730950488016887242096980786 z = 0.84089641525371454303112547623321489504 誤差 x: 4.8801688724209698078596369014359646933 E-17 y: 3.5119831127579030192140409244737913280 E-16 z: 5.6968874523766785104960243772098740359 E-17 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python Selenium Chrome driverの自動更新のやり方

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

Jupyter Notebook上でPipenvで作成した仮想環境を使えるようにする

目的 Pipenvで作成した仮想環境をJupyter Notebook上で活用できるようにするメモ書きです。 環境 Windows 10 Python 3.6 version 2020.11.15 仮想環境の作成とカーネルの追加 仮想環境を構築し、Jupyter Notebookをインストールをインストールします。 > pip install pipenv # pipenvをインストールします。 > pipenv --python 3.6 # プロジェクトを初期化 > pipenv install jupyter # Jupyter Notebookをインストール カーネルをJupyter Notebookに追加します。 > pipenv shell # 仮想環境に入る > ipython kernel install --user --name=<カーネル名> --display-name=<表示名> # カーネルの追加 > jupyter notebook # Jupyter Notebookの起動 以上の設定でJupyter NotebookからPipenvでインストールしたモジュールなどが使用可能となります。 ※補足:python 3.8によるカーネルエラー 上記手順ではpythonのバージョンを3.8にした場合、Jupyter Notebook上でカーネルエラーとなってしまいます。 解決策が分ければ追記します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Huggingface Transformers 101本ノック 31本目~35本目 FacebookのDeiT(Data-efficient image Transformers)

概要 Huggingface Transformers 101本ノック 21本目~30本目 OpenAI CLIPでは画像認識の未知なるラベルを効率的に事前学習する手法CLIP(Contrastive Language-Image Pre-training)に関する問題を解いてきた。事前学習モデルとしては、ViT(Vision Transformer)を利用していた。今回は、Facebookチームが開発したDeiT(Data-efficient image Transformers)を利用して画像認識を行う。Deitもtransformerベースの事前学習モデルだが、ViTやEfficientNetなどと比較すると、より少ないデータ・少ないパラメタで学習が行えるように「蒸留トークン」という考え方を導入している。詳細は、原著を参照のこと。 対象読者 人工知能・機械学習・深層学習の概要は知っているという方 理論より実装を重視する方 Pythonを触ったことがある方 Huggingface Transformers 101本ノック1本目~3本目を打ち終えた方 Huggingface Transformers 101本ノック4本目~7本目を打ち終えた方 Huggingface Transformers 101本ノック8本目~10本目を打ち終えた方 Huggingface Transformers 101本ノック11本目~13本目 Accuracy Recall Precision F1スコアによる評価を打ち終えた方 Huggingface Transformers 101本ノック 14本目~17本目 GPT2による日本語作文を打ち終えた方 Huggingface Transformers 101本ノック 18本目~20本目 BERTでMLM(Masked Language Model)を打ち終えた方 Huggingface Transformers 101本ノック 21本目~30本目 OpenAI CLIPを打ち終えた方 目次 1.31本目-ライブラリのセットアップ 2.32本目-使用する画像データサンプルの取得 3.33本目-事前学習モデルのロード 4.34本目-前処理 5.35本目-推論(画像認識) 6.参考文献 7.著者 8.参考動画 9.他のノックはこちら 1. 31本目 ライブラリのセットアップ 質問: ColabでDeiTを利用するために必要なモジュールをインストールせよ。 回答: huggingfaceのtransformersのみ入れれば、依存するものは入ってくる。ver.は結構大事なのでつけておくことを推奨する。 pip install transformers==4.6.1 2. 32本目 使用する画像データサンプルの取得 質問: 猫画像のサンプルを取得しpillowのImageにロードせよ。 回答: https://cocodataset.org/#explore を利用する。pillowとrequestsを用いることでインターネット上に公開されている画像は簡単にpillowのimageにロード可能。 from PIL import Image import requests url = 'http://farm8.staticflickr.com/7250/7520201840_3e01349e3f_z.jpg' image = Image.open(requests.get(url, stream=True).raw) image 3. 33本目 事前学習モデルのロード 質問: DeiT(Data-efficient image Transformers)の事前学習モデル'facebook/deit-base-distilled-patch16-224'をDeiTFeatureExtractor, DeiTForImageClassificationを利用しロードせよ。 回答: feature_extractorとmodelにロードする。feature_extractorは、画像認識の前処理を担当させ、modelの方で推論(画像認識)タスクを解かせる。 from transformers import DeiTFeatureExtractor, DeiTForImageClassification feature_extractor = DeiTFeatureExtractor.from_pretrained('facebook/deit-base-distilled-patch16-224') model = DeiTForImageClassification.from_pretrained('facebook/deit-base-distilled-patch16-224') 4. 34本目 前処理 質問: 32本目でロードした画像を前処理にかけ、DeiTの事前学習モデルへ渡せる形式に変換せよ。 回答: feature_extractorを利用する。 inputs = feature_extractor(images=image, return_tensors="pt") 前処理された後の画像はmatplotlibで簡単に確認できる。 import matplotlib.pyplot as plt plt.imshow(inputs['pixel_values'][0][0]) 5. 35本目 画像認識 質問: 34本目で前処理をしたinputsを入力し、DeiTForImageClassificationを利用して画像認識を行え。認識結果は索引を変換したテキスト(キャプション・ラベル)として出力せよ。 回答: 推論結果はlogitsで返却されるため、argmaxをとって、最も大きい値の索引を返却することで、キャプションに変換できる outputs = model(**inputs) logits = outputs.logits predicted_class_idx = logits.argmax(-1).item() print(f'Predicted Class:{model.config.id2label[predicted_class_idx]}') 結果 Predicted Class:car wheel 猫と認識してくれなかったのだが、横に置いてある自転車の車輪の方を車の車輪として 認識したようだ。個人的にはCLIPの方が実用的に思えた。 11. 参考文献 Huggingface model hub -原著論文 -Facebook AI Blog 12. 著者 ツイッターでPython/numpy/pandas/pytorch関連の有益なツイートを配信してます。 @keiji_dl 13. 参考動画 直感!Pytorchで始める深層学習実装入門(実践編) 7ステップで作るPython x Flask x Pytorch 人工知能Webアプリ開発入門 14. 他のノックはこちら Huggingface Transformers 101本ノック1本目~3本目 Huggingface Transformers 101本ノック4本目~7本目 Huggingface Transformers 101本ノック8本目~10本目 Huggingface Transformers 101本ノック11本目~13本目 Accuracy Recall Precision F1スコアによる評価 Huggingface Transformers 101本ノック 14本目~17本目 GPT2による日本語作文 Huggingface Transformers 101本ノック 18本目~20本目 BERTでMLM(Masked Language Model) Huggingface Transformers 101本ノック 21本目~30本目 OpenAI CLIP
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dictの比較クイズ

dictの比較クイズ はじめに ほとんどのプログラミング言語ではキーと値を対応させたデータ構造を扱うことができます。 JavaではMap、JavaScriptではObject、Pythonではdictなどです。 今回はPythonのdictの比較の挙動が一部予想外だったのでクイズ形式で紹介します。 他の言語での挙動 JavaScriptのObjectの比較は非常にシンプルです。 参照が異なる場合は、たとえ中身が一緒であったとしてもfalseとなります。 const person1 = {name: "taro"}; const person2 = {name: "taro"}; console.log(person1 == person2); > false 逆に参照が等しければ、代入後に値を書き換えてもtrueになります。 const person3 = person1; person3.name = "jiro"; console.log(person1 == person3); > true それではPythonのdict比較クイズに挑戦してみましょう。 クイズ 第一問 中身が異なるdictを比較すると当然、Falseになります。 person1 = {"name": "taro"} person2 = {"name": "taro", "age": 20} person3 = {"name": "jiro"} print(person1 == person2) > False print(person1 == person3) > False では値がすべて等しいdictを比較するとどうなるでしょうか? person1 = {"name": "taro", "age": 20} person2 = {"name": "taro", "age": 20} print(person1 == person2) > ??? 第一問の正解はこちら person1 = {"name": "taro", "age": 20} person2 = {"name": "taro", "age": 20} print(person1 == person2) > True 予想外だったのではないでしょうか? Pythonのdictの比較は、参照の比較ではなく、中の値まで比較します。 第二問 dictの値にさらにdictがある場合はどうなるでしょうか?あるいは値に配列がある場合はどうでしょうか? ただし値に指定されているdictや配列の中身は等しいです。 person1 = {"name": "taro", child: {"name": "jiro"}} person2 = {"name": "taro", child: {"name": "jiro"}} print(person1 == person2) > ??? person3 = {"name": "taro", children: [{"name": "jiro"}]} person4 = {"name": "taro", children: [{"name": "jiro"}]} print(person3 == person4) > ??? 第二問の正解はこちら person1 = {"name": "taro", "child": {"name": "jiro"}} person2 = {"name": "taro", "child": {"name": "jiro"}} print(person1 == person2) > True person3 = {"name": "taro", "children": [{"name": "jiro"}]} person4 = {"name": "taro", "children": [{"name": "jiro"}]} print(person3 == person4) > True ともにTrueです。なんとなく傾向がつかめてきました。 第三問 ネストさせるのは第二問と同様ですが、person1、person2のchildにperson1を入れます。 この場合はどうなるでしょうか? person1 = {"name": "taro"} person1["child"] = person1 person2 = {"name": "taro"} person2["child"] = person1 print(person1 == person2) > ??? ちなみにこのとき、person1やperson2はchildで無限に参照することができます。 person1["child"] > {'name': 'taro', 'child': {...}} person1["child"]["child"] > {'name': 'taro', 'child': {...}} person1["child"]["child"]["child"] > {'name': 'taro', 'child': {...}} これを踏まえて、答えがどうなるか予想してみてください。 第三問の正解はこちら person1 = {"name": "taro"} person1["child"] = person1 person2 = {"name": "taro"} person2["child"] = person1 print(person1 == person2) > True おそらく比較の実装がdictの中身の値のを比較する前に、参照が等しいかどうかを判定していると予想することができます。 第四問 第三問と似ていますが、person1のchildにはperson1、person2のchildにはperson2をそれぞれ詰めます。 今回、この記事を投稿しようと思ったのはこの第四問の挙動が面白かったからなので、ぜひ予測してみてください。 person1 = {"name": "taro"} person1["child"] = person1 person2 = {"name": "taro"} person2["child"] = person2 print(person1 == person2) > ??? 第四問の正解はこちら person1 = {"name": "taro"} person1["child"] = person1 person2 = {"name": "taro"} person2["child"] = person2 print(person1 == person2) > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > RuntimeError: maximum recursion depth exceeded in cmp エラーになりました。再帰呼び出しが上限に到達したというエラーです。 参照が異なるとネストを無限にたどってしまうのですね。 さいごに どのくらい正解したでしょうか?予想外な挙動があって勉強になった、面白かったって方はぜひLGTMしてね♥
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Django超初心者攻略記事その1

概要 Djangoへこれから触れる方への超基礎・概念説明 その1 概念 Webフレームワークの1つ 例:ルールに従い、作成するとWebアプリができるよ!メールやデータベースの変更も簡単にできるよ! 1つのフォルダの中にプロジェクトとして作成していく。 作成の流れ Python+Djangoインストール ユーティリティコマンドで、基本的な部分を自動生成する。:プロジェクトの生成 django-admin startproject アプリケーションの生成 python manage.py statapp Pjファイルの直化にアプリファイルを作る 同じファイル名が作られるのはPJ用とアプリ用です。 次の流れ データベース設定を行う ロギングの設定(ログの出力先など)を行う ルーティングの設定(http:/hoge/huga/ + login or main などURLの末尾で処理や飛ばすイメージ) ビューを設定する。アプリケーションの!views.pyにクラス変数を設定する。 テンプレートを設定する。アプリケーションのtemplatesフォルダにてhttpを設置する 開発サーバー起動し確認する。 ここまでが土台です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

androidにTermux+adb+sshで画像処理自動化をスタンドアローンで動かせる説が浮上!!

0. 企画の概要 現在,下記の画像のようにゲームの自動化を行っています.まず,最初に,スマホ画面をスクリーンショットしてからPCに送信します.次に,画像解析を行ったのちにクリックコマンドをスマホに送信します.最後にスマホ上でクリックされてゲームが進んでいく感じです. 下記の動画はこの構成でスマホゲームの自動化を行っています. しかし,これだと自動化するためにPCが必要であったり,ケーブルが必要であったりと結構手間です.そこで,今回は下記の図のようにTermux内にubuntuの環境を構築し,そこで画像処理を行えるようにすることでandroid単体で画像処理自動化プログラムを回していこうとする企画です. 1. Termuxのもろもろの設定 1.1 Termuxの基本設定 アプリストアからインストール. そしたら, パッケージのアップグレード. pkg upgrade 完了したらSDカードの機能をONにする. termux-setup-storage これでTermuxの基本設定は完了. << 参考サイト >> 【随時更新】個人的 Termuxセットアップメモ https://zenn.dev/ryuu/scraps/6b2584294674b7 1.2 SSHの基本設定 android上でコマンドを打つのはとても面倒なので, SSH経由でandroidに接続し, PC上をコマンドを叩くようにする. *参考サイトの通りに行えば特に問題なく動作したので割愛させていただきます. * << 参考サイト >> Termux on AndroidのSSHサーバに接続する方法 https://linuxfan.info/termux-sshd 1.3 ubuntuのインストール temux上でadbコマンドを叩くようにするには結構面倒くさそうです. そこで, Ubuntuを導入して, そのUbuntu上でadbコマンドを叩くような流れでやっていきます. *参考サイトの通りに行えば特に問題なく動作したので割愛させていただきます. * << 参考サイト >> 【随時更新】個人的 Termuxセットアップメモ https://zenn.dev/ryuu/scraps/6b2584294674b7 2. adbコマンドの導入と設定 2.1 temuxのUbuntuにadbコマンドを導入 adbコマンドのインストール >sudo apt-get install android-tools-adb adbがインストールされたか確認 >adb version adbコマンドでandroid端末が認識されているか確認. ポップアップが出るので許可してください. >adb devices List of devices attached emulator-5554 device << 参考サイト >> UbuntuでAndroid Debug Bridgeを使ってみる https://saitodev.co/article/Ubuntu%E3%81%A7Android_Debug_Bridge%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/ 2.2 sshクライアント側のPCにadbコマンドを導入 ssh接続するPC側にもadbコマンドの導入をお願い致します. ubuntuなら上記のコマンドで同様にインストールが可能です. winodwsとかだとandroid studioとかに入っているのでそれを使ってください. 2.3 PCからandroidにadb接続 接続します. $ adb devices 端末にADB接続していいかアラートが表示されるのでOKします. これでWi-Fi接続の許可をします. $ adb tcpip 5555 IPの確認を下記コマンドで行います. $ adb shell ip route ここで表示されたIPをメモしておきます. 2.4 termuxからandroidにadb接続 下記のコマンドでtermuxから自身のandroidにadb接続ができます. >adb connect <ip:port> この状況になれば, 今まで自動化で使用していたadbコマンド達を使用できそうです. << 参考サイト >> Android端末内のTermuxからその端末にADB接続する https://tanokatu.com/2019/05/20/01/ 3. 自動化関連のadbコマンド使ってみる 自動化関連で使用していたのはスクリーンショットコマンドと画面タッチコマンドの2つです. なので,この2つのコマンドがtermux上で使い物になるかを検証していきたいと思います. 3.1 スクリーンショットコマンド 実際に下記のスクリーンショットコマンドを叩いてみました. adb shell screencap -p /sdcard/screen.png その結果,ちゃんとスクリーンショットが撮影されていました. 3.2 Termuxのバックグラウンド起動 このままでは,ゲーム画面に遷移するとTermuxが落ちてしまいます.そこで,Termuxをバックグラウンド起動に設定します. 僕のスマホでは 設定>バッテリー>アプリ>Termux>バックグラウンド起動のON でいけました. 3.3 タッチコマンドの確認 現在進行中.........
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ModuleNotFoundError: No module named 'widget_tweaks'

'widget_tweaks'モジュールを使用してマイグレーションを行う際に”ModuleNotFoundError: No module named 'widget_tweaks'”が発生する時。 答え pip3 install django-widget-tweaks pip3でインストールを行うと正常にマイグレーションが開始される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WARNING: `pyenv init -` no longer sets PATH.

WARNING: pyenv init - no longer sets PATH. web にある記事に従って pyenv をインストールしようとすると、以下の内容を .bashrc や .zshrc に記載するように書かれていることが多い export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" これを反映すると以下のように WARNING が出る パスの変更も反映されないのでこの状態で pyenv global などを実行しても反映されない % source .zshrc WARNING: `pyenv init -` no longer sets PATH. Run `pyenv init` to see the necessary changes to make to your configuration. 対処 .zshrc もしくは .bashrc を以下のように変更する export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" # pyenv init - から pyenv init --path に変更 eval "$(pyenv init --path)" 反映 % source .zshrc
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

肺炎を検知するディープラーニングアプリケーション

初めての投稿になります。 お手柔らかにお願いします。 目的 自分自身の理解を深めるため (いないと思うけど)深層学習について良く知りたい、という人で右も左もわからないという人が検索で引っかかったなりした時にちょっとでも役立ちたいため。 参照先 Kaggle Notebook(旧Kernel)でディープラーニングを実装する。Kaggleの登録方法などは省略する。といってもアクセスすれば英語が読みさえすれば明快というか、登録ボタンなどを押せばいいだけなのだが。 Kaggle Notebookについて知らない人向けに解説すればJupitar Notebookと同じようなものです。Jupiter Notebookすら知らない人向けにいえばPythonやRのコードを見栄えよく並べて簡単に実行できたりするオープンプラットフォーム(paiza.ioと似たもの)みたいなものです。 アプリの説明 データセットは胸部のレントゲン写真です。そのデータから学習し、入力したデータ先の画像に対して平常の可能性(確率)と肺炎の可能性(確率)を棒グラフ化して可視化することがゴールです。 そもそも深層学習とは何か/Introduction 人間の脳のシナプスやニューロンの働きを基に作られていると言われています。 シナプスを初めて通過した情報(電気信号)は通過するのに時間がかかりますがその時に刺激されて発生した電気信号はニューロンに貯蓄されます。そうして別の酷似した信号が来た時にシナプス(管)を通りやすくします。そうやって重複した情報の処理を速くすることで生き物の記憶、認識のシステムは稼働しています。この原理を利用します。 まずレイヤー(層)について紹介します。 Introduction2:層 ハンバーガーをイメージしましょう。上と下の生地の一方を入力層、出力層とします。間の色々は中間層と呼びます。 〇は中継地点で活性化関数から出力した数値にウエイトが掛けられバイアスが加算されます。 入力層の数と出力層の数はそろっている必要はありません。 こうした構造はニューロンネットワークと呼ばれます。 線は脳構造でいうシナプスで情報が伝っていくルートをさし、情報が伝っていくことを伝播(でんぱ)するといいます。 伝播の仕方として偏ることはありません。 Introduction3:活性化関数と損失関数 活性化関数:ニューロンの興奮/抑制状態を決める関数 例1 step関数 例2 ReLu関数 損失関数(Error、Loss):出力と正解の間の「誤差」を定義する関数 例 2乗和誤差関数 他 NNLoss, 交差エントロピー誤差関数、Adam関数 順伝播、逆伝播 上述のような伝播の仕方を順伝播といいます。一度出力した損失分からウエイト、重みを更新することを逆伝播といいます。経路を辿って後ろから関数を変換するわけですから逆伝播というわけです。その変換の仕方も1種類ではなく多種様々です。 最適化アルゴリズム criterion(正規化)、optimizer(最適化)、scheduler(調節)の3つ 正規化とは値を0から1までに変換する。 最適化とはパラメータを調整し誤差を最小化しようとする 調節は最適化関数の中にある変数を変えていく。言い方を変えれば必要に応じて学習効率を変えていく、といっていい。(初学の段階では外してもよい) 最適化手法の例 確率的勾配降下法(SGD) 実際のコードに触れてみよう インポート import numpy as np import pandas as pd import torch import os import matplotlib.pyplot as plt import torch.nn.functional as F from torch import nn,optim from torchvision import transforms as T,datasets,models from torchvision.utils import make_grid from torch.utils.data import DataLoader from collections import OrderedDict from tqdm import tqdm pd.options.plotting.backend = "plotly" numpy, pandasは配列の計算をしやすくするPythonのライブラリ matplotlibは画像を描画するライブラリでpyplotはそのメソッド 後の説明は省略するが transformsはTと略して用いることだけ強調しておく。 GPU GPUとはディープラーニング、画像認識用のもっと早く計算できる演算装置のこと。 ※デプロイする際にGPUだと不都合なこともある def get_device(use_gpu): if use_gpu and torch.cuda.is_available(): # これを有効にしないと、計算した勾配が毎回異なり、再現性が担保できない。 torch.backends.cudnn.deterministic = True return torch.device("cuda") else: return torch.device("cpu") # デバイスを選択する。 device = get_device(use_gpu=True) ローディング データはコンピュータ世界では全て数値で判断する。 特に画像データはRGB(色の3原色、Red, Green, Blue)を数値指定する(0 ~ 255, 255は2の8乗-1) data_dir = "../input/chest-xray pneumonia/chest_xray/chest_xray" TEST = 'test' TRAIN = 'train' VAL ='val' 1行目はパス指定(Jupiter Notebookでのコード実行を想定) それ以外はネーミング設定 def data_transforms(phase = None): if phase == TRAIN: data_T = T.Compose([ T.Resize(size = (256,256)), T.RandomRotation(degrees = (-20,+20)), T.CenterCrop(size=224), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]) ]) elif phase == TEST or phase == VAL: data_T = T.Compose([ T.Resize(size = (224,224)), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]) ]) return data_T trainset = datasets.ImageFolder(os.path.join(data_dir, TRAIN),transform = data_transforms(TRAIN)) testset = datasets.ImageFolder(os.path.join(data_dir, TEST),transform = data_transforms(TEST)) validset = datasets.ImageFolder(os.path.join(data_dir, VAL),transform = data_transforms(VAL)) class_names = trainset.classes print(class_names) print(trainset.class_to_idx) T.Compose[...](transforms.Compose)はよく使われる。何をしているかといえば画像をコンピューターが変換できるように処理をする。 決まり文句に近いものでよく使われるので 前処理と称されることが多い。 大まかに言えば 訓練データかそうでないかで場合分け 各々 ・サイズ設定(resize) ・切り抜き(crop) ・平均と分散(散らばりぐあい)で標準化 ・テンソル化(pandas, numpyで画像データの数値配列操作をしやすくする、数学用語のテンソル化と無理に結びつける必要はない) 再びネーミングづけ。validは検証を示す。 trainset = datasets.ImageFolder(os.path.join(data_dir, TRAIN),transform = data_transforms(TRAIN)) testset = datasets.ImageFolder(os.path.join(data_dir, TEST),transform = data_transforms(TEST)) validset = datasets.ImageFolder(os.path.join(data_dir, VAL),transform = data_transforms(VAL)) class_names = trainset.classes print(class_names) print(trainset.class_to_idx) いったん話を元に戻す 訓練データ、検証データ、テストデータ 「訓練データ、検証データ」と「テストデータ」と捕らえていただきたい。 実用的に考えて実行ボタンをクリックするたびにゼロからスタートするのでは時間がかかりすぎる。またAIの仕組みは未知のデータに対し予め学習されたデータから推測を行うことである。 その学習用のデータを訓練データといい、テストデータはチェックをするデータである。今回は予め分けられている状態で扱うが、場合によっては7:3程度の割合で分けるところから実装を始めることもある。もちろん訓練データの方が多く、分け方はアトランダムである。 訓練データを元データをコピーするなどして元データ100%で学習させた場合、過学習といって「元データに適応しすぎたため、かえって応用しづらくなる」といわれる。(ここに関して、私は、人の認識とコンピュータ演算結果にギャップがあったために生じる都合である、と解釈した。つまり人が重視しない、見落とした情報をコンピュータが拾うため違和感のある出力データを発するということ。) 追記:要はアニメの原理と同じで静止画を高速で見せられると動いてるように脳が錯覚するのと発想の根本は同じ。 テストデータはお膳立てが一通り終わった後に使用するスタンドアローンなものだが、検証データというのは学習効率の調整(schedular)を行うためのデータである。元データと大きく外れたものと是正するためのデータである 再びコードレビュー、データ拡張 学習不足を防ぐために簡単な操作でデータ量を増やすこと。 画像の回転、肉付けなどなど。長くなるのでここでは深く解説しない。 0はNormal, 1はPheumonia(肺炎を指す) def plot_class_count(classes,name = None): pd.DataFrame(classes,columns = [name]).groupby([classes]).size().plot(kind = 'bar',title = name).show() def get_class_count(dataset,name = None): classes = [] for _,label in dataset: if label == 0: classes.append(class_names[label]) elif label == 1: classes.append(class_names[label]) return classes trainset_class_count = get_class_count(trainset,name = 'trainset_classes_count') plot_class_count(trainset_class_count,name = 'trainset_classes_count') ラベリング。 ※ size()はデータの型を表す。 plot()はグラフ用にデータ化。 show()は実際に描画。 trainloader = DataLoader(trainset,batch_size = 16,shuffle = True) validloader = DataLoader(validset,batch_size = 8,shuffle = True) testloader = DataLoader(testset,batch_size = 8,shuffle = True) shuffle= Trueで任意の順番でデータを取り出す(ロードする) エポックとバッチ バッチ(batch) 入力と正解のペア(ここではサンプルと呼ぶ)の集合 バッチごとに学習が行われる 「バッチサイズ」はバッチに含まれるサンプルの数 1エポック分の訓練データは、複数のバッチに分割される エポック(epoch) 全ての訓練(学習)データを1回学習することを、1エポックと数える 1エポックで、訓練データを全て使い切る バッチ学習とオンライン学習 バッチ学習 バッチサイズは訓練データの数に等しい  全ての訓練データを1度の学習で使い切る 欠点は学習が不正確になる。感覚的には途中まではサインカーブ(正弦曲線)の描画を学習しようとしたときに途中からパターンが違ったら対応できないよね...ってこと。 オンライン学習 バッチサイズは1で、1エポックの学習回数は訓練データの数に等しい ・欠点は逐一時間がかかる。感覚的にはfpsが低いのと同じ。 ミニバッチ学習 両者の良いとこどり。 訓練データを小さなサイズのバッチに分割し、バッチごとに学習する。 実際に描画 Dataloder: データセットからデータをバッチサイズ ごとにまとめて返す ```python def show_image(image,title = None,get_denormalize = False): image = image.permute(1,2,0) mean = torch.FloatTensor([0.485, 0.456, 0.406]) std = torch.FloatTensor([0.229, 0.224, 0.225]) image = image*std + mean image = np.clip(image,0,1) if get_denormalize == False: plt.figure(figsize=[15, 15]) plt.imshow(image) if title != None: plt.title(title) else : return image dataiter = iter(trainloader) images,labels = dataiter.next() out = make_grid(images,nrow=4) show_image(out, title=[class_names[x] for x in labels]) ``` iter(イテレータ)とはデータを取り出す道具のこと。meanは中心のこと。こうしておくと入力するデータ数値の扱いの見栄えが良くなる。 stdは偏差のこと。 再びコードから離れる。CNNの説明 画像認識の実装部分としてscikitlearnを使って画像を学習するだとか、CNN、RNNだとか多様である。 まず画像の情報はいずれにせよRGBの情報を示す数値である。 ここではCNNの説明をする。 CNNは畳み込みニューラルネットワークという。 オーソドックスにいえば畳み込み→プーリング→全結合層(前と後ろで双方全てつながっている層)→出力 出力は各グループに分類される確率となる 畳み込みとプーリング CNN = Covolutional Neural Network フィルタはコンピュータ用の虫眼鏡だと思えばいい。画像の中で拡大した領域上で大事な部分を抜き出す。 必然的に元の情報のピクセルは大きくなり「網目」は荒くなる。 model = models.vgg16() for param in model.parameters(): param.requires_grad = False classifier = nn.Sequential(OrderedDict([('fc1', nn.Linear(25088, 4096)), ('relu', nn.ReLU()), ('dropout',nn.Dropout(0.3)), ('fc2', nn.Linear(4096, 4096)), ('relu', nn.ReLU()), ('drop', nn.Dropout(0.3)), ('fc3', nn.Linear(4096, 2)), ('output', nn.LogSoftmax(dim = 1))])) model.classifier = classifier model.to(device) ※ドロップアウト 過学習を避けるためニューロンネットワークの途中であえてデータの取り捨てを行うこと。例えば3割の確率で情報を捨てる。確率なのだからエポックごとに同じ箇所でだけ消去されるというようなことはない。 ※Linearの変数は例えば(4096, 2)であれば4096個のニューロンから2個のニューロンからなる層にデータが伝播しますよ。の意。 ※classifierはclassifyの派生語、分類器などの訳語があてられる。 ※nnはニューラルネットワークの略。冒頭のインポート参照。 ※toはint型に変換しますよ...の意。(だったかな?) お約束として最後にソフトマックス関数(活性化関数の一種)に入れて調節の仕上げをする。 vgg16 有名なCNNのモデルの一種。13層のCNN層と3層の全結合層からなる。 他、プーリング 他、パディング(0で囲むこと)、プーリングなどあるが詳細は割愛する。 プーリング:画像の各領域を代表をする値(最大値など)を取り出して、出力画像にする  損失分を計算 Accuracyは正確さ 損失関数はライブラリの中のメソッドを使用。 .trainは訓練モード .evalは評価モード topk関数 ・確率の高い順位k番目まで出力する。今回は1番目のみ criterion = nn.NLLLoss() optimizer = optim.Adam(model.parameters(),lr = 0.001) schedular = optim.lr_scheduler.ReduceLROnPlateau(optimizer,factor = 0.1,patience = 5) epochs = 15 valid_loss_min = np.Inf def accuracy(y_pred,y_true): y_pred = torch.exp(y_pred) top_p,top_class = y_pred.topk(1,dim = 1) equals = top_class == y_true.view(*top_class.shape) return torch.mean(equals.type(torch.FloatTensor)) for i in range(epochs): train_loss = 0.0 valid_loss = 0.0 train_acc = 0.0 valid_acc = 0.0 model.train() for images,labels in tqdm(trainloader): images = images.to(device) labels = labels.to(device) ps = model(images) loss = criterion(ps,labels) optimizer.zero_grad() loss.backward() optimizer.step() train_acc += accuracy(ps,labels) train_loss += loss.item() avg_train_acc = train_acc / len(trainloader) avg_train_loss = train_loss / len(trainloader) model.eval() with torch.no_grad(): for images,labels in tqdm(validloader): images = images.to(device) labels = labels.to(device) ps = model(images) loss = criterion(ps,labels) valid_acc += accuracy(ps,labels) valid_loss += loss.item() avg_valid_acc = valid_acc / len(validloader) avg_valid_loss = valid_loss / len(validloader) schedular.step(avg_valid_loss) if avg_valid_loss <= valid_loss_min: print('Validation loss decreased ({:.6f} --> {:.6f}). Saving model ...'.format(valid_loss_min,avg_valid_loss)) torch.save({ 'epoch' : i, 'model_state_dict' : model.state_dict(), 'optimizer_state_dict' : optimizer.state_dict(), 'valid_loss_min' : avg_valid_loss },'Pneumonia_model.pt') valid_loss_min = avg_valid_loss print("Epoch : {} Train Loss : {:.6f} Train Acc : {:.6f}".format(i+1,avg_train_loss,avg_train_acc)) print("Epoch : {} Valid Loss : {:.6f} Valid Acc : {:.6f}".format(i+1,avg_valid_loss,avg_valid_acc)) 追記中。。。(6/6現在)  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[kaggle / python] kaggleで回帰問題の超々々初歩から取り組む(2)

前回の続きです。 前回の記事はこちら。 Summary of the previous article(前回のあらまし) 前回は、kaggle の House Prices の課題に対して、 「xgboostのmodel.fitにそのままでは渡せない列を、全て削除する」 という雑な仕事をやってのけました。 そんな雑な仕事で上位68%になんか入ってしまった、、、というところで終わりました。 (せいぜい90%くらいだと思ってました) Today's result 今回は、LabelEncoderを使って、 「前回削除していた列を、全て使える状態へと置換した上で、xgboostのモデル作成をする」 ここまでやってみました。 その結果、上位 68% から 64% へと上昇しました!(((o(*゚▽゚*)o))) (総プレイヤー数が増えているので、順位は落ちている) 前回 今回 さて、具体的に何をしたのか見ていきましょう~ Second step 1. source cord 1-0. same as the previous cord 前回と同じソースは、この「1-0.」にまとめておきます(^ワ^*) やっていることは、 モジュール読み込み データ読み込み 説明変数と目的変数の分割 学習データとテストデータの分割 です。 # import modules import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error from xgboost import XGBClassifier, XGBRegressor # load data import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) train = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/train.csv') test = pd.read_csv('/kaggle/input/house-prices-advanced-regression-techniques/test.csv') # split data into explanatory variable(説明変数) and response variable(目的変数) tmp_train_x = train.drop('SalePrice', axis=1) tmp_train_y = train['SalePrice'] # split data into training data and test data x_train, x_test, y_train, y_test = \ train_test_split(tmp_train_x, tmp_train_y, test_size=0.20, random_state=0) 1-1. replace the variable not used in first step (by LabelEncoder) 前回の記事では、以下のmodel.fitに弾かれたカラムを全て学習データから削除していました。 削除したカラムはこれら。 not_expected_type_column_names = [ 'MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour', 'Utilities', 'LotConfig', 'LandSlope', 'Neighborhood', 'Condition1', 'Condition2', 'BldgType', 'HouseStyle', 'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType', 'ExterQual', 'ExterCond', 'Foundation', 'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2', 'Heating', 'HeatingQC', 'CentralAir', 'Electrical', 'KitchenQual', 'Functional', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual', 'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'MiscFeature', 'SaleType', 'SaleCondition' ] 今回は、これらのカラムも全て学習データとして扱えるように置換してみます。 何も考えなくてもできそうな方法を参考書籍から探してみると、label encodingなるものを発見。 まずはこれから試していこう。 1 置換を行うためにも、型を見てみましょう。 print(x_train[not_expected_type_column_names].dtypes) MSZoning object Street object Alley object ...(略)... SaleType object SaleCondition object dtype: object どうやらいずれのカラムもobject型だったみたいです。 xgboostの学習データにobject型(文字列)が投入できないということを、今更知りました(*ノωノ) なんとなく気になったので、どんな文字列データが入っているのか見てみました。(1行目だけ) print(x_train[not_expected_type_column_names].iloc[0].values) ['RL' 'Pave' nan 'Reg' 'Lvl' 'AllPub' 'Inside' 'Gtl' 'NridgHt' 'Norm' 'Norm' '1Fam' '1Story' 'Hip' 'CompShg' 'CemntBd' 'CmentBd' 'BrkFace' 'Ex' 'TA' 'PConc' 'Ex' 'TA' 'Av' 'GLQ' 'Unf' 'GasA' 'Ex' 'Y' 'SBrkr' 'Gd' 'Typ' 'Gd' 'Attchd' 'Unf' 'TA' 'TA' 'Y' nan nan nan 'New' 'Partial'] うむ、どのカラムも文字列が入っていると.... ....あ?いや、nanがありますね...(∩´﹏`∩) 実は最初、nanが学習データに入ったままの状態でlabel encodingにかけようとしたのですが、以下のエラーが発生しました。 --------------------------------------------------------------------------- ...(略)... During handling of the above exception, another exception occurred: TypeError Traceback (most recent call last) <ipython-input-97-dd5b0af638b6> in <module> 11 target_all_data_column = df_all_data[col_name] 12 le = LabelEncoder() ---> 13 le.fit(target_all_data_column) 14 if col_name == 'MSZoning': print(target_all_data_column.unique()) 15 ...(略)... /opt/conda/lib/python3.7/site-packages/sklearn/preprocessing/_label.py in _encode(values, uniques, encode, check_unknown) 112 res = _encode_python(values, uniques, encode) 113 except TypeError: --> 114 raise TypeError("argument must be a string or number") 115 return res 116 else: TypeError: argument must be a string or number LabelEncoder()インスタンスのfit関数に、nan入りのデータを食わせたところで、 TypeError: argument must be a string or numberが発生してます。 これを避けるために、nanを文字列に置換してからlabel encoderにかけます。 2 from sklearn.preprocessing import LabelEncoder x_train_label_encoded = x_train.copy() x_test_label_encoded = x_test.copy() # ※注意 df_all_data = pd.concat([x_train, x_test, test]) for col_name in not_expected_type_column_names: # nanを'NaN'という文字列に置換してます target_all_data_column = df_all_data[col_name].fillna('NaN') le = LabelEncoder() le.fit(target_all_data_column) if col_name == 'MSZoning': print(target_all_data_column.unique()) target_train_column = x_train[col_name].fillna('NaN') target_test_column = x_test[col_name].fillna('NaN') x_train_label_encoded[col_name] = le.transform(target_train_column) x_test_label_encoded[col_name] = le.transform(target_test_column) x_train_label_encoded.head() Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape ... MiscFeature MiscVal MoSold YrSold SaleType SaleCondition 618 619 20 4 90.0 11694 1 1 3 ... 1 0 7 2007 7 5 870 871 20 4 60.0 6600 1 1 3 ... 1 0 8 2009 9 4 92 93 30 4 80.0 13360 1 0 0 ... 1 0 8 2009 9 4 817 818 20 4 NaN 13265 1 1 0 ... 1 0 7 2008 9 4 302 303 20 4 118.0 13704 1 1 0 ... 1 0 1 2006 9 4 メモ ~Label Encodingを行う上での注意~ 「※注意」と書いたところには少し手こずりました。 問題は、x_testやtestに入っているラベルが、必ずしもx_trainに入っているわけではないということでした。 label encodingを行う上では、存在する全ての種類のラベルをエンコーディングできないと困ります。 もし、x_trainに入っているラベルだけをfitに食べさせてしまうと、x_trainには存在しないが、x_testやtestには存在するラベルが認識できない、というエラーが起きます。 こんなエラーです。 ValueError: y contains previously unseen labels: 'RRAn' というわけで、label encoderのfit関数には、学習データもテストデータも込みで、全ラベルを食べさせてあげる必要がありました。 落ち着いて考えれば当たり前なのですが、データ分析の経験が浅いこともあり、こんなことにも気付かなかったです(_ _;) 1-2. create model and try prediction ! あとは、1-1でlabel encodingにかけたデータでXGBRegressorを学習させてあげるだけです。 ここは変数名以外は前回と一緒です。 # create model(学習) model = XGBRegressor(n_estimators=20, random_state=0) model.fit(x_train_label_encoded, y_train) # prediction(推論) predict_result_for_tr_data = model.predict(x_train_label_encoded) predict_result = model.predict(x_test_label_encoded) さて、prediction結果を点数にしてみましょう! # 学習データをpredictした結果 rmse_of_train = np.sqrt(mean_squared_error(predict_result_for_tr_data, y_train)) rmse_of_train 8146.100714171564 # テストデータをpredictした結果 rmse_of_test = np.sqrt(mean_squared_error(predict_result, y_test)) rmse_of_test 33584.266159693645 前回は9283.22...と37010.13...だったので、いずれも誤差の値(RMSE)が小さくなっていていい感じです^^* 1-3 make submission data 最後に、kaggleに提出するデータを作ります。 test_encoded = test.copy() for col_name in not_expected_type_column_names: # ここでも、全データをlabel encoderのfitに食べさせるのを忘れずに! target_all_data_column = df_all_data[col_name].fillna('NaN') le = LabelEncoder() le.fit(target_all_data_column) target_test_column = test[col_name].fillna('NaN') test_encoded[col_name] = le.transform(target_test_column) predict_submission = model.predict(test_encoded) submission = pd.DataFrame( {'Id': test['Id'], 'SalePrice': predict_submission} ) submission.to_csv('submission_new.csv', index=False) この結果が上位64%でした~(((o(*゚▽゚*)o))) 大して上位に行ったわけではないですが、目に見える結果が出ると嬉しいですね! Next step 次やりたいことはこの辺の記事の中から選んでいこうと思っています。(適当) https://medium-s.jp/kaggle-house-prices/ https://qiita.com/yyokii/items/a5a772bf4f32183a3501 まずは、部屋の総面積を表すカラムを作成したらどうなるか試してみたい。 その次に、重要度の低いカラムを削除した場合に結果がどう変わるかを見ていきたい。 References 統計WEB / 1-5. 説明変数と目的変数 説明変数と目的変数の英語訳は、この記事を見て真似しました。 explanatory variable: 説明変数 response variable: 目的変数 こういう単語がわかってると、英語の関連記事が読みやすくなるはず(`・ω・´)キリッ 参考文献 Kaggleで勝つデータ分析の技術 シリーズ一覧 [kaggle / python] kaggleで回帰問題の超々々初歩から取り組む(1) [kaggle / python] kaggleで回帰問題の超々々初歩から取り組む(2)<- 本記事 one hot encodingが使えることは知っていたのですが、列数が増えてしまって手がかかりそうなのと、xgboostしか使わないのであれば、one hot encodingする必要はないと聞いたので、今回は却下  ↩ 次の二つの記事を見て、nanを文字列に置換することにしました ①【Teratail】sklearn の Label Encoderでカテゴリカル変数の前処理をしたい、②【stackoverflow】LabelEncoder — TypeError: argument must be a string or number ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

実務を彩るNaive-Bayes ~自社HP判定モデル~

こんにちは、スクラムサインの大塚です。私はスクラムサインのインターン生として、Naive-Bayesを用いたクラス分類アルゴリズムを実装していました。今回はNaive-Bayesの解説と、実務で実際にどのようにそれを利用したかをまとめて紹介いたします。 目次 ・まず、Naive-Bayesとはそもそも何か? 1.ベイズの定理 2.Naive-Bayesの数式モデル 3.NaiveなBayesの意味とは? ・Pythonを用いた実装方法 1. データの用意 2. データの前処理 3. 学習 4. 予測 5. 精度の評価 ・まとめ まず、Naive-Bayesとはそもそも何か? Naive-Bayesとは、ベイズの定理をもとにした、教師あり学習アルゴリズムのことです。 特に分類タスク向けのアルゴリズムで、文書データに対してよい結果を出すことが知られています。 このNaive-Bayesについて数式的な面からまずは解説していきます。 1.ベイズの定理 まずは、このNaive-Bayesのもとになっているベイズの定理から解説します。 ある事象Aが起こる確率を$P(A)$、ある事象が起こる確率を$P(B)$とします。 このとき、$B$が起こった上で$A$が起こる確率を、$P(A|B)$のように表すことができ、これを $B$が与えられた時の$A$の事後確率と言います。 ベイズの定理とは、この事後確率に関する定理で、以下の数式で表されます。 $$ P(A|B) = \frac{P(B|A)P(A)}{P(B)} $$ つまり、関連ある事象の事前確率から、ある事象の確率を記述する方法を導く定理です。 2.Naive-Bayesの数式モデル それでは、Naive-Bayes本体の数式モデルを見ていきます。 あるクラス変数$y$と、その特徴量である$x_1, x_2, \cdots x_n$において、ベイズの定理から $$ P(y|x_1, x_2, \cdots ,x_n) = \frac{P(x_1, x_2, \cdots ,x_n|y)P(y)}{P(x_1, x_2, \cdots ,x_n)} $$ が成り立ちます。 しかし、右辺の分子に存在する$P(x_1, x_2, \cdots ,x_n|y)$を考えることは非常に難しいです。なぜなら、$P(x_1, x_2, \cdots, x_n)$つまり、$x_1$から$x_n$という$n$個の特徴ベクトルを持つ確率を考えることはできないからです。 そこで、「クラス値が与えられれば」という前提のもと、条件付き独立を仮定します。 つまり、 \begin{align} P(x_1, x_2, \cdots ,x_n|y) &=&P(x_1|y) P(x_2|y) \cdots P(x_n|y)\\ &=& \prod_{i = 1}^{n} P(x_i| y) \end{align} と仮定してしまうわけです。 すると、先ほどの式は \begin{align} P(y|x_1, x_2, \cdots ,x_n) &=& \frac{P(x_1, x_2, \cdots ,x_n|y)P(y)}{P(x_1, x_2, \cdots ,x_n)}\\ \\ &=& \frac{P(y)\prod_{i = 1}^{n} P(x_i| y)}{P(x_1, x_2, \cdots ,x_n)} \end{align} と変換することができます。 ここで、右辺の分母である$P(x_1, x_2, \cdots ,x_n)$は定数であるため、 \begin{align} P(y|x_1, x_2, \cdots ,x_n) &=& \frac{P(x_1, x_2, \cdots ,x_n|y)P(y)}{P(x_1, x_2, \cdots ,x_n)}\\ \\ &=& \frac{P(y)\prod_{i = 1}^{n} P(x_i| y)}{P(x_1, x_2, \cdots ,x_n)}\\ &\propto& P(y)\prod_{i = 1}^{n} P(x_i| y) \end{align} という比例の関係が成り立ちます。 つまり、ある特徴量$x_1, x_2, \cdots, x_n$が与えられた時にあるクラス変数$y$である確率は、 $P(y)\prod_{i = 1}^{n} P(x_i| y)$に比例するということが言えます。 学習させたデータを元にテストデータの特徴量からこれを計算させることで、テストデータがどのクラスに属するか予測することができます。 各クラスにおいて$P(y)\prod_{i = 1}^{n} P(x_i| y)$の値が一番大きい$y$がそのテストデータの予測クラスです。 つまり、予測したクラスを$\hat{y}$とすると、Naive-Bayesのモデル式は $$ \hat{y} = arg max P(y)\prod_{i = 1}^{n} P(x_i| y) $$ のように表すことができます。 3.NaiveなBayesの意味とは? これで、Naive-Bayesに関する数式的な評価はできました。 しかし、このNaiveとはどういう意味なのでしょうか。 naiveとは、日本語で「単純な」と言う意味です。では、どこが単純なのでしょうか? それは、このアルゴリズムの特徴的な部分である条件付き独立の仮定にあります。 例えば、プログラミングの記事について考えるとします。この記事は「プログラミング」と言うクラスに属し、特徴量としてその記事に含まれる単語を持つとします。 この記事において、「Python」という単語がでてきたら、同時に「変数」や「条件分岐」など、同時に含まれる確率が高いであろう単語を考えることができます。逆に、「トマト」や「お米」など、同時に含まれる確率が低いであろう単語もあります。 このように、文書に含まれる単語間の関係は実際は無視できません。 しかしこのNaive-Bayesでは、クラスが与えられていると言う前提のもと、これらの単語の関係は全て無視され、単語間の関係は全て独立であると考えます。 この仮定によって、先ほどの数式のような考えやすいシンプルな数式でクラス分類アルゴリズムをを実装することができます。 これが、NaiveなBayesと呼ばれる理由となっています。 もちろん、この仮定が現実で成り立つことはありえませんが、実応用上ではかなり優れた力を発揮します! これは、誤った独立性仮定による誤りの増加よりも、独立性仮定によってパラメータ数を減らしてパラメータの推定精度を向上させたことによる誤りの減少が勝っているからではないかと考えられます。 ※ただし、この仮定のために、Naive-Bayesによって推定した確率値はあてにならないといわれているので注意! Pythonを用いた実装方法 それでは、実際にPythonでNaive-Bayesを実装してみます。 私がこのNaive-Bayesを用いて実装していた機械学習モデルは、 Googleで特定の会社や施設を検索して、検索結果に出てきた上位10ページが「自社によるHP」か「他社によるHP」か分類するモデルです。 この実装は、 1. データの用意 2. データの前処理 3. 学習 4. 予測 5. 精度の確認 の順に紹介したいと思います。 1. データの用意 今回作成するモデルは、googleの検索結果から自社サイトか他社サイトか分類するものです。そのため、それらの分類に必要と考えられるデータは、 description (掲載サイトの説明部分) url (サイトurl) title (サイトタイトル) 自社HPかどうか (自社なら1、他社なら0の2値データ) comp_name (調べたい会社や施設などの名前) の5種類です。 次の具体例は、実際に使用した教師データの中の1つになります。 description: 『神戸地方検察庁. ... 検察官又は検察庁をかたった虚偽公告にご注意ください。 神戸地方検察庁からのお知らせ一覧 ... 兵庫県警察本部 · 兵庫県弁護士会 ... 〒650-0016 神戸市中央区橘通1丁目4番1号 電話:078-367-6100(代表). Copyright (C) ...』 url: 『http://www.kensatsu.go.jp/kakuchou/kobe/kobe.shtml』 title: 『神戸地方検察庁』 自社HPかどうか: 『1』 comp_name: 『神戸地方検察庁』 このようなデータの集まりを自社HPかどうかで二つに分けて、それぞれを教師データとすることで教師あり学習を行います。 2. データの前処理 次に、先ほどのデータを扱いやすいように加工します。今回の場合、データの前処理は以下の順番に行いました。 文字列の置換・削除 形態素解析による単語ごとの分割 単語のカウント 単語のベクトル化 文字列の置換・削除 まずは、タイトルやurl,descriptionを学習させやすいように変換します。 例えば、descriptionやtitleにふくまれる法人名はデータによって違いますが、これをそれぞれの法人名から「法人名」という文字列へ置換することで、すべてのデータへ適応可能な学習をすることができます。 また、url(『http://www.kensatsu.go.jp/kakuchou/kobe/kobe.shtml 』)をそのまま利用すると、urlが長い一つの単語とみなされてしまいます。そのため、urlの中の「/」や「.」をスペースに変換し、「https:」や「www」を消すことで、urlを意味のある単語の集団(『kensatsu go jp kakuchou kobe kobe shtml』)として扱うことができます。 他にも、 半角と全角を統一など文字表現の正規化 固有名詞を「固有名詞」と置換 数詞、記号、助詞を削除 など様々な処理を行いました。これで、文字列の置換・削除は完了です。次は特徴量となる単語カウントをするために単語ごとに分割していきます。 形態素解析による単語ごとの分割 学習データとして利用するために、descriptionなどに含まれる単語を特徴量としてカウントする必要があります。 この際に、例えば英語の場合「I have a pen」のように単語ごとにスペースで区切られているため、単語のカウントは容易です。 しかし、日本語の場合は、「私はペンを持っています」のように単語ごとに区切られていないため、そのままでは単語ごとにカウントをすることができません。そのため、言葉が意味を持つまとまりの単語(形態素)に分割する形態素解析をする必要があります。 そこで、今回は形態素解析エンジンであるMeCabを用いて形態素解析しました。mecab-python3というライブラリをインストールすると、PythonでMeCabを利用できます。 このMeCabを用いて形態素解析すると、先ほどの「私はペンを持っています」は、「私 は ペン を 持って います」のように分割されることになります。 実際は先ほどの作業で行った文字列の置換により、置換されたり助詞は削除されたりした意味があると考えられる単語の集まりになっています。これで単語の分割は完了しましたので、実際に次は単語のカウントに移っていきます。 単語のカウント・one-hotベクトル化 次は、先ほど分割した単語を仕分けしていきます。この際に、単語をベクトル化することで仕分けていきます。 例えば犬、猫、豚という3種類の単語を考えます。単語カウントの際に、それぞれの単語を[1, 0, 0],[0, 1, 0], [0, 0, 1]のというように単語を1つの成分が1で残りの成分が全て0であるようなベクトル(これを、one-hotベクトルといいます)で表します。こうすることで、変数の全ての値を平等に扱うことができます。この変換には、TensorFlowを用いました。 このようにして、全ての単語に関してone-hotベクトル化をおこなって、リストの各要素がone-hotベクトルであるリストのリストである二次元リストを作成します。あとは、これらを一つにまとめる作業をすれば文書ごとの単語カウントを作成できます。 出現単語カウントを一つにまとめる 最後に、先ほど作成したone-hotベクトルを一つにまとめます。 先ほどの犬、猫、豚のone-hotベクトルについて、例えば一つの文書が[1,0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0], [0, 1, 0], [1, 0, 0]を要素として持っているとします。 これを列に対して全て足すと、[3, 2, 1]となり、犬が3回、猫が2回、豚が一回出てきている文書ということになります。 このような作業を全文書に対して行うことで、文書ごとの出現単語カウントを要素に持つ1つの二次元リストXが出来上がります。 これらを足す操作は、Numpyのsum関数を用いて行いました。 あとは、それぞれの文書が自社HPかどうかを要素に持つyを用意することで、教師あり学習を行う準備が整いました。 また、先ほどのX,yを訓練用とテスト用にX_train, X_test, y_train, y_testのように分割しておくことで、学習とは別にテスト精度を評価することができます。 3. 学習 実際にNaive-Bayesを用いて学習をしていきます。Pythonには、scikit-learnという多くの機械学習アルゴリズムが手軽に実装できるライブラリがあります。これを用いると、簡単にNaive-Bayesを利用することができます。 scikit-learnでは多数のNaive-bayesアルゴリズムを使えますが、メジャーなのは次の3つです。 GaussianNB BernoulliNB MultinomialNB そして、これらの使い分けは簡単に言うと、 クラスが与えられた条件で特徴量の分布が正規分布していると仮定できる場合は、GaussianNB 特徴量が0,1の2値で表される時はBernoulliNB 単語カウントなど、特徴量が連続値ではなく多数の値をとる時はMultinomialNB と考えることができます。 今回のデータの場合、 特徴量は記事に含まれる単語カウントの集まりなので、MultinomialNBを用いた学習を行います。 学習は、MultinomialNBのfitメソッドで行うことができます。 from sklearn.naive_bayes import MultinomialNB #MultinomialNBのインスタンス化 multi = MultinomialNB() #先ほど作成した訓練用データで学習 multi.fit(X_train, y_train) このように、scikit-learnを使うと、かなり短いコードでNaive-Bayesを利用することができます。 これで学習は終わりましたので、次はようやく予測をしてみようと思います! 4. 予測 予測には、先ほど学習させたmultiというインスタンスのpredictメソッドを使います。 先ほど使用した訓練用データとは別に用意したテストデータを用いてテストし、精度を確認します。 #テスト用データに対して予測する pred = multi.predict(X_test) これで、predには、これらのテストデータの各文書に対して予測したクラスの数値が格納されます。 print(pred) #[1, 0, 0, 1, 0, ..., 1, 0]のように各文書ごとの予測クラスが格納されています。 これで、予測も完了しました。あとは、予測したデータがどれくらい正解しているのか確認してみましょう! 5. 精度の評価 一般的に教師ありクラス分類の精度を評価するには、混同行列と呼ばれる表を用います。 混同行列とは、あるデータを分類したときに、その正解・不正解の数を整理しておく表です。 今回の場合、テストデータに対して予測をした結果が先ほどのpred、そのテストデータの正しいクラス分類が格納されているのが、2.で作成したy_testです。 この2つのデータを比較し、実際に私が作ったクラス分類モデルのテストデータに対する混同行列を作成すると次のようになりました。これは、sklearn.metricsモジュールの、confusion_matrix関数を用いて作成することもできます。 実際の自社HP 実際の他社HP  合計 自社HPと予測 25 34 59 他社HPと予測 13 432 445 合計 38 466 504 これをみると、他社HP466個のうち、正しく他社HPと予測できたのは432個であり、自社HP38個のうち、正しく自社HPと予測できたのは、25個であることがわかります。 また、誤判定も少ないことからNaive-Bayesは単純でありながら実務でもなかなか良い結果を出すことがわかります。 このように、教師ありクラス分類モデルの精度評価は、混同行列を用いると正判定・誤判定を平等に評価することができます。 まとめ 今回は、Naive-Bayesに関する数式的な解説を載せたうえで、Pythonによる実装方法を実務と関連づけながら簡単に紹介しました。結果を見ると、Naive-Bayesは簡潔なモデルながら現実の問題でもかなりの力を発揮することがわかると思います。 機械学習の技術は、日々すさまじいスピードで進歩しています。ライブラリの利用により、簡単に機械学習が実装できてしまうこの時代の中で、表面的に理解をするのではなく、内部の本質を理解していくことの重要性を学びました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

getattrを使った新規作成 or 更新処理

OneToOneのモデルの新規作成、更新処理を書いてたらへんなバグ踏んだのでメモ。 前提 class Parent(models.Model): pass class Child(models.Model): parent = models.OneToOneField(Parent, on_delete=models.CASCADE) こんな感じの1対1のモデルがあり、すでにChildがある場合は更新、ない場合は新規作成したい。 結論 parent = Parent.objects.get(id=1) child = getattr(parent, 'child', None) or Child(parent=parent) # ...中略... child.save() こう書く。 蛇足 やりたいことを愚直に書くとこんな感じ。 parent = Parent.objects.get(id=1) if Child.objects.filter(parent=parent).exists(): child = parent.child else: child = Child(parent=parent) # ...中略... child.save() リレーション先の存在確認は hasattr でできるので、 if文はもう少し短くなる。 if hasattr(parent, 'child'): child = parent.child else: child = Child(parent=parent) 三項演算子で書くと1行にまとまる。 child = parent.child if hasattr(parent, 'child') else Child(parent=parent) 失敗コード getattr使えば更に短くなると思って以下のように書いた。 child = getattr(parent, 'child', Child(parent=parent)) 新規作成時には問題ないが、更新時にsaveすると例外が出た。 django.db.utils.IntegrityError: UNIQUE constraint failed: my_app_child.parent_id getattrの第3引数(デフォルト値)の評価は判定前に行われるらしく、 Child(parent=parent) の時点で OneToOneField なので parent.child がすでに設定されているものから新しく作られたものに差し替えられてしまうのが原因っぽい。 # すでに child.id=1 のオブジェクトが紐付け済み >>> parent.child <Child: Child object (1)> # 戻り値は `parent.child` だが、第3引数の評価が先に行われるので `parent.child` が新しく作ったオブジェクトになる >>> getattr(parent, 'child', Child(parent=parent)) <Child: Child object (None)> >>> parent.child <Child: Child object (None)> Child(parent=parent) はリレーションがない場合だけ実行してほしいので、結論の通り短絡評価使えばシンプルに書ける。 ただし、getattr使う場合はデフォルト値の指定がなくてリレーション先のオブジェクトがないと RelatedObjectDoesNotExist の例外が飛ぶ。 >>> parent2 = Parent.objects.create() <Parent: Parent object (2)> >>> hasattr(parent2, 'child') False # デフォルト値がないと例外吐く >>> getattr(parent2, 'child') Traceback (most recent call last): File "<console>", line 1, in <module> File ".../django/db/models/fields/related_descriptors.py", line 421, in __get__ raise self.RelatedObjectDoesNotExist( my_app.models.Parent.child.RelatedObjectDoesNotExist: Parent has no child. # デフォルト値渡してあげると大丈夫 >>> getattr(parent2, 'child', None)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【強化学習】【基本編】1.MDPとベルマン方程式

強化学習が大好きなので記事を書いてみることにしました。お手やわやわにお願いします。 目次 0.はじめに 1.強化学習の問題設定 2.マルコフ決定過程 3.ベルマン方程式 4.おわりに 0.はじめに こんにちは、強化学習大好きマンのかんといいます。 強化学習を勉強する上で一番辛いのは、数学的なややこしさだと個人的に思っていて特に序盤で詰まって諦めることが多いのかなと思います。実際自分がそうで、序盤の意味がわからなさすぎて半年くらい放置してました。 強化学習は数学がとても難しいと言われていて、自分も式の意味レベルくらいの理解で厳密な数学的理解はできていないところもあります。それでも強化学習は本当に面白い分野だと思っているので布教したくて記事を書こうと思いました。 できるだけ具体例をまじえて勉強中の人はもちろん強化学習について何も知らないという人でもわかるよう書くつもりです。 間違いやもっとわかりやすい書き方などがありましたら指摘していただけるととても助かります。よろしくお願いします。 強化学習の最初の最初なのでこの記事は長くなると思いますが最後まで読んでいただけると嬉しいです。 では、具体的なゲームを通して強化学習の問題設定から解説してみたいと思います。 1.強化学習の問題設定 あなたは今から友達とこの迷路(と呼べるのか?)を使ってゲームをしようとしています。この迷路にはルールがあって 2,3のマスでは1回の行動で右か左に1マス動ける。1のマスから左に動くと確率pでいきなりゴールへ行けて、確率1-pで1に戻る。(普通に右に行くこともできる) 別のマスに移動したとき-1ポイントされるが、ゴールに到達したときは-1ポイントではなく10ポイントもらえる。1で左に行き1に戻ってきた場合も-1ポイント。 最初スタートする場所を1,2のどちらかから選べる。 ゴールするまでに得たポイントの合計が高い人が勝ち というものです。 さて、1,2のどちらのマスからスタートするのが勝てそうでしょうか。pが大きいほど1からスタートした方がよいことはわかりますが具体的にpが何以上なら1からスタートするべきでしょうか。 2から右に行き続けたとき-1+10=9の9ポイントでゲームを終了するので、期待値が9より大きい時は1からスタートして左に行った方が良さそうです。1に戻ってきてしまい-1ポイントされても途中で諦めず必ず左に行く選択をする場合、1回目で行けるとき確率はpでポイントは10、2回目で行ける時確率は(1-p)pでポイントは-1+10、3回目で行ける時確率は(1-p)(1-p)pでポイントは-1-1+10、4回目で行ける時は、、、と考えると期待値Eは $$E = p \times10+(1-p)p\times(-1+10)+(1-p)(1-p)p\times(-1-1+10)+・・・$$ $$=p\sum_{n=0}^{\infty}(1-p)^n(-n+10)=10p\sum_{n=0}^{\infty}(1-p)^n+p(1-p)\frac{d}{dp}\sum_{n=0}^{\infty}(1-p)^n $$ $$=10-\frac{1}{p}+1=11-\frac{1}{p} $$ となって11-1/p>9を計算するとpが1/2より大きい時は1からスタートした方がよいことがわかります。 (計算をかなりはしょりましたごめんなさい、(-n+10)を分配法則で分けて10の方は普通に計算、-nの方はpで(1-p)^nを微分すれば-nが下に降りてくることを利用しました) 面白みのないゲームを例に取りましたが、このように 1.ある状態から(ここではマス) 2.行動を選択し(ここでいう右に行くか左に行くかを決定し) 3.次の状態への確率的な遷移をして(ここでいう1マスの1-p,pのこと、普通のマスでは確率1で行きたい方向に移動すると解釈できる。同じマスにいても行動によって行く可能性のあるマスや、確率が違うことに注意。例えば1で左という行動を選択すれば1-pで1,pでゴールだが右を選択すれば確率1で2に移動) 4.報酬を得る(ここでいうポイントのこと) というような1,2,3,4の過程を繰り返して得られる報酬の和を最大にするような行動の仕方(このマスにいる時はこう行動する方がいいといったような)を求めよう!というのが強化学習の目的です。 2.マルコフ決定過程 強化学習の根幹ともいえるマルコフ決定過程(Markov decision process 略してMDP)の話です。大まかには上の1,2,3,4の過程のことですが用語をまじえながら改めて説明をしてみます。 まずはよく出てくる用語の説明 エージェント:強化学習において実際にある状態で意思決定を行い、上のような1,2,3,4の過程を経験する存在のこと 状態:英語でstateなのでsとよくかかれる。上のゲームでいうマスのこと 行動:英語でactionなのでaとよくかかれる。上のゲームでいう右に行く、左に行く 方策:よくπと書かれる。エージェントがある状態sで行動aを選ぶ確率のこと。この書き方からわかる通り条件付き確率なのでπ(a|s)とも書かれる。本や記事によっては政策と書かれているものもある。 (状態)遷移確率:エージェントがある状態sで行動aを選んだとき次の状態s'にいく確率のこと。この書き方からわかる通り条件付き確率なのでp(s'|s,a)とも書かれる 報酬:英語でrewardなのでよくrと書かれる。エージェントがある状態に遷移した時に得るポイントのこと。状態s,s'、行動aで決まるものなのでr(s,a,s')と書かれることもある。 ※図では状態s'が3個しかないが何個でもOK 図のようにしてエージェントが状態sから方策π(a|s)にしたがって行動aを決定し、状態遷移確率p(s'|s,a)にしたがって次の状態s'にいって報酬rを得る。そしてまた方策にしたがって、、を繰り返す過程をマルコフ決定過程といいます。 注意しなければならないのは行動を決定する際、考慮するのは今いる状態sのみで今まで経験してきた状態やしてきた行動は考慮しないということです。このような性質をマルコフ性といいます。(方策がπ(a|s)と書かれている通り今いる状態sしか考慮していません。) エージェントが報酬をr1,r2,r3、、と得る時r1+r2+r3+...を最大にする方策πを探すのが強化学習の目的ということになります。 3.状態価値関数 さて、強化学習らしくなってきました。 強化学習では現在エージェントがいる状態をs、遷移した次の状態をs'とよく書きますが、 エージェントが状態を次々に移動していくのを意識して時刻tにおける状態を$s_t$と書きます。 s'は$s_{t+1}$ということになりますね。 この添字tを使えばマルコフ決定過程は時刻tの状態$s_t$において、方策πにしたがって行動$a_t$を決定し、遷移確率$p(s_{t+1}|s_t,a_t)$にしたがって状態$s_{t+1}$に遷移し、報酬$r_{t+1}$を受け取るのを繰り返す過程のことです。 強化学習の目的はすべてのtに対して時刻tからの報酬の和 $\sum_{n=1}^{\infty}r_{t+n}$を最大にする方策を見つけることになります。(大事なので何回も言わせてください) ここでエージェントの気持ちになって考えると遠い未来(上のシグマでいうnが大きい時)の報酬は直近の報酬より魅力が少ない気がしませんか? ぼくはすぐ目先の利益ばかり考えて行動してしまいます。 ということで時刻tから見てnステップ先に得られる報酬は$\gamma^{n-1}$をかけて割り引くことにします。 つまり次の状態での報酬は割り引かず、2個先の報酬は$\gamma$をかけて割り引く、3個先の報酬は$\gamma^2$をかけて割り引く、4個先の報酬は、、といった感じです。 こうすると時刻tにおける最大にしたい報酬の和は$\sum_{n=1}^{\infty}\gamma^{n-1}r_{t+n}$とかけます。 (が0<$\gamma$<1の時は目先のこと重視、1<$\gamma$の時は将来を重視、となりますね。) この$\sum_{n=1}^{\infty}\gamma^{n-1}r_{t+n}$は割引報酬和といってよく$G_t$とかかれます。 時刻tからの割引報酬の和でこれを最大化する方策を見つけるのが強化学習の(略) さてこの$G_t$ですが、次の状態$s_{t+1}$での割引報酬和$G_{t+1}$を考えると $$G_t=r_{t+1}+\gamma G_{t+1}$$ と書けることがわかります。 時刻tにおいての状態$s_t$から得られる割引報酬和$G_t$は次の状態$s_{t+1}$に遷移した時に得られる報酬$r_{t+1}$と$\gamma$で割引された状態$s_{t+1}$から得られる割引報酬和$G_{t+1}$を足したものであるということです。 もっと噛み砕いて言うと時刻t以降で得られる報酬=時刻t+1で得られる報酬+時刻t+1以降で得られる報酬ということになります。 ところが一番上のようなゲームを考えると確率的な遷移があるので、同じ方策で行動しても割引報酬和$G_t$は試行をするたびに変動することが予想されます。 これではどうやっていい方策を求めればよいかわからないので時刻tにおける状態$s_t$がある状態$s$で、ある方策$\pi$にしたがって行動するときの$G_t$の(条件付き)期待値$E_\pi[G_t|s_t=s]$を最大にするような方策を見つけることにします。 つまり1回程度の試行では負けるかもしれないけど何回も行えば最終的な報酬の和が最大になるような方策を見つけようということになります。これが強化学(略) はい、ということで$E_\pi[G_t|s_t=s]$を最大化しようということですね。 ところでエージェントの気持ちになると時刻tにおいて2つの状態$S_1$と$S_2$があった時に、 そこから得られると予想される報酬、$E_\pi[G_t|s_t=S_1]$と$E_\pi[G_t|s_t=S_2]$を比べた時にその値が大きい状態の方が価値が高いと思いませんか? 例えば、2つの状態$S_1$と$S_2$は1番上のゲームの1,2マスと思ってください。 上のゲームでは実際に期待値を計算しました。 あの値はまさにマス1にいる時は確率1で左に行くという方策、書くとすれば方策が「$\pi_{絶対左}$」のもとでの$E_{\pi_{絶対左}}[G_t|s_t=マス1]$の値です。 計算の結果からpが1/2より大きい時マス1から始めた方がよいとなりました。 つまりpが1/2より大きい時はマス2よりマス1の方が価値が大きいといえないか、ということです。 強化学習では$E_\pi[G_t|s_t=s]$は状態$s$の価値であるとして状態価値(関数)という名前がついており$\upsilon_\pi(s)$と書きます。 つまり、任意の時刻tで状態$s$から方策$\pi$にしたがって行動したとき得られる割引報酬和の期待値ということで、その値こそが状態sの価値だということです。 $$\upsilon_\pi(s)=E_\pi[G_t|s_t=s]$$ 注意したいのは方策$\pi$の関数でもあるということです。 (p>1/2でマス1からスタートしてもずっと右に行き続けるという方策では割引報酬和は下がりますよね) この$\upsilon_\pi(s)$をすべての$s$について最大にする方策$\pi$を見つけ(略) (用語を解説するにつれて「強化学習の目的」がだんだん難しくなっている気がしますごめんなさい) 4.ベルマン方程式 ここが鬼門です。以前自分はここで意味がわからなくなって半年放置してました どうしても難しくなってしまいそうですが、できるだけ噛み砕いて解説するのでついて来てください! では上の$G_t=r_{t+1}+\gamma G_{t+1}$の式の両辺の期待値をとって状態価値関数に関する式を作ってみましょう。 ただしこの式は時刻tにおいての状態$s_t$がある$s$で同じであるという前提 (右辺で$s_t=s$でない、つまり時刻tにおける状態$s_t$が左辺と違うなんてことはないはずです) なので条件付き期待値をとって(方策$\pi$にしたがって行動することも忘れずに) $$E_\pi[G_t|s_t=s]=E_\pi[r_{t+1}+\gamma G_{t+1}|s_t=s]$$ 状態価値関数の定義 $\upsilon_\pi(s)=E_\pi[G_t|s_t=s]$と期待値の線型性から $$\upsilon_\pi(s)=E_\pi[r_{t+1}|s_t=s]+\gamma E_\pi[G_{t+1}|s_t=s]$$ まず$E_\pi[r_{t+1}|s_t=s]$の項からみていきましょう。 この式の意味は時刻tにおける状態$s_t$がある状態$s$であるとき、次の状態に遷移してもらえる報酬rの期待値ですよね。 方策$\pi$と遷移確率で確率がかなり絡んでいるのでややこしそうです。 とる行動a、次の状態s'、そのときもらえる報酬r(s,a,s')を固定して考えてみましょう。 r(s,a,s')がもらえるのはエージェントが確率$\pi(a|s)$で行動aを選び遷移確率$p(s'|s,a)$でs'に遷移するときなので確率$\pi(a|s)\times p(s'|s,a)$でこの報酬r(s,a,s')をもらえます。 これはどのa,s',r(s,a,s')でも言えるので期待値は$\pi(a|s)\times p(s'|s,a)\times r(s,a,s')$をすべてのa,s'で足し合わせればよいことになります。 つまり $$E_\pi[r_{t+1}|s_t=s]=\sum_{a} \Bigl(\sum_{s'}\bigl(\pi(a|s)p(s'|s,a)r(s,a,s')\bigr)\Bigr)$$ $$=\sum_{a} \Bigl(\pi(a|s)\sum_{s'}\bigl(p(s'|s,a)r(s,a,s')\bigr)\Bigr)$$ ($\sum_{n}$はすべてのnで足し合わせるという意味です。$\sum_{a},\sum_{s'}$はすべての$a,s'$で和を取るという意味です。) $\pi(a|s)$はs'の変数ではないのでs'でのシグマから出して上のような式になります。 続いて$E_\pi[G_{t+1}|s_t=s]$ですが、この式の意味は時刻tにおける状態$s_t$がある状態$s$のとき、時刻t+1以降で得られる割引報酬和の期待値ですよね。 t+1以降で得られる割引報酬和の期待値は、次の状態を$s'$とすると状態価値関数の定義から$\upsilon_\pi(s')$とかけますね。 状態$s'$に行き着く確率は上と同じように$\pi(a|s)\times p(s'|s,a)$なので求める期待値は$\pi(a|s)\times p(s'|s,a)\times \upsilon_\pi(s')$をすべての$a,s'$で足し合わせたものになります。 式にすると $$E_\pi[G_{t+1}|s_t=s]=\sum_{a} \Bigl(\sum_{s'}\bigl(\pi(a|s)p(s'|s,a)\upsilon_\pi(s')\bigr)\Bigr)$$ $$=\sum_{a} \Bigl(\pi(a|s)\sum_{s'}\bigl(p(s'|s,a)\upsilon_\pi(s')\bigr)\Bigr)$$ 考え方は上の$r(s,a,s')$の時と同じですね。 基本的な式の意味は$G_t=r_{t+1}+\gamma G_{t+1}$と変わってなくて時刻t以降で得られる報酬=時刻t+1で得られる報酬+時刻t+1以降で得られる報酬だけど、期待値なので確率で重みづけして和を取っているというだけです。 以上から $\upsilon_\pi(s)=E_\pi[r_{t+1}|s_t=s]+\gamma E_\pi[G_{t+1}|s_t=s]$ は $$\upsilon_\pi(s)= \sum_{a} \Bigl(\pi(a|s)\sum_{s'}\bigl(p(s'|s,a)r(s,a,s')\bigr)\Bigr)+\gamma \Bigl(\sum_{a}\pi(a|s)\sum_{s'}\bigl(p(s'|s,a)\upsilon_\pi(s')\bigr)\Bigr)$$ $$=\sum_{a} \Bigl(\pi(a|s)\sum_{s'}\bigl(p(s'|s,a)r(s,a,s')+\gamma p(s'|s,a)\upsilon_\pi(s')\bigr)\Bigr)$$ $$=\sum_{a} \Bigl(\pi(a|s)\sum_{s'}p(s'|s,a)\bigl(r(s,a,s')+\gamma \upsilon_\pi(s')\bigr)\Bigr)$$ と変形できます。 本によってはかっこがなかったり、$\pi$が$\sum_{s'}$の中に入っていたりしますが意味は同じですね。 改めて見てみると$\sum$内の $r(s,a,s')+\gamma \upsilon_\pi(s')$ が $G_t=r_{t+1}+\gamma G_{t+1}$の右辺に対応して、上述のこの説明の意味がわかりやすくなったかなと思います。 基本的な式の意味は$G_t=r_{t+1}+\gamma G_{t+1}$と変わってなくて時刻t以降で得られる報酬=時刻t+1で得られる報酬+時刻t+1以降で得られる報酬だけど、期待値なので確率で重みづけして和を取っているというだけ 今導いたこの式がベルマン方程式と呼ばれているもので、強化学習の基本的な方程式です。 $$\upsilon_\pi(s)=\sum_{a} \Bigl(\pi(a|s)\sum_{s'}p(s'|s,a)\bigl(r(s,a,s')+\gamma \upsilon_\pi(s')\bigr)\Bigr)$$ これからこの式を使って上のゲームでどういう方策をとればマスの状態価値を最大にできるのか、その方策をどうやって求めるのかという話になっていきます。 楽しくなってきましたね、このゲームではもう最強になれるかもしれません。 やろうとはならないけど 4.おわりに お疲れ様でした。 結構重めの内容だったとは思いますが、ここまで読んでいただいてありがとうございます。 次回も重めな内容にはなりそうですが、できるだけわかりやすく強化学習の面白さを伝えたいと思っていますので読んでもらえると嬉しいです。 わかりにくい所や間違っている所があれば指摘していただけるととても助かります、よろしくおねがいします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Scipyのsolve_ivpで条件を満たすまでシミュレーションを行う

概要 scipyライブラリのsolve_ivpのevent機能について試すために自由落下のシミュレーションを行います。 今回は地面に到着した瞬間までシミュレーションを行います。 式 自由落下の式は次のようになります。 \begin{align} \frac{dy}{dt} &= v \\ \frac{dv}{dt} &= -mg \end{align} ここで、物体の高さを$y$、速度を$v$とします。 $m$と$g$は定数です。 $y=0$のとき地面と衝突したと考えます。 solve_ivpのeventはfloatを返す関数で定義され、eventの発生時に0を返します。 今回はそのまま物体の高さ$y$を返します。 プログラム 上記の式を用いてシミュレーションプログラムを作成します。 from scipy.integrate import solve_ivp import numpy from matplotlib import pyplot # 物理定数 m = 1 g = 1 def free_fall(t, y): ''' 自由落下の方程式 ''' return numpy.asarray([y[1], -m * g]) def ground(t, y): ''' 地面との衝突の判定式 ''' return y[0] ground.terminal = True # 条件が満たされたらシミュレーションを終了する if __name__ == '__main__': # 初期値 y0 = numpy.asarray([10, 0]) # 0から10までの時間を100分割してシミュレーションを行う sol = solve_ivp(free_fall, [0, 10], y0, t_eval=numpy.linspace(0, 10, 100), events=ground) print(sol.t) print(sol.y) # グラフ出力 pyplot.plot(sol.t, sol.y[0]) pyplot.ylabel('y') pyplot.xlabel('t') pyplot.show() 結果 プログラムを実行すると次の出力とグラフが得られます。 [0. 0.1010101 0.2020202 0.3030303 0.4040404 0.50505051 0.60606061 0.70707071 0.80808081 0.90909091 1.01010101 1.11111111 1.21212121 1.31313131 1.41414141 1.51515152 1.61616162 1.71717172 1.81818182 1.91919192 2.02020202 2.12121212 2.22222222 2.32323232 2.42424242 2.52525253 2.62626263 2.72727273 2.82828283 2.92929293 3.03030303 3.13131313 3.23232323 3.33333333 3.43434343 3.53535354 3.63636364 3.73737374 3.83838384 3.93939394 4.04040404 4.14141414 4.24242424 4.34343434 4.44444444] [[10. 9.99489848 9.97959392 9.95408632 9.91837568 9.87246199 9.81634527 9.75002551 9.6735027 9.58677686 9.48984797 9.38271605 9.26538108 9.13784308 9.00010203 8.85215794 8.69401082 8.52566065 8.34710744 8.15835119 7.9593919 7.75022957 7.5308642 7.30129579 7.06152433 6.81154984 6.55137231 6.28099174 6.00040812 5.70962147 5.40863177 5.09743904 4.77604326 4.44444444 4.10264259 3.75063769 3.38842975 3.01601877 2.63340475 2.2405877 1.8375676 1.42434445 1.00091827 0.56728905 0.12345679] [ 0. -0.1010101 -0.2020202 -0.3030303 -0.4040404 -0.50505051 -0.60606061 -0.70707071 -0.80808081 -0.90909091 -1.01010101 -1.11111111 -1.21212121 -1.31313131 -1.41414141 -1.51515152 -1.61616162 -1.71717172 -1.81818182 -1.91919192 -2.02020202 -2.12121212 -2.22222222 -2.32323232 -2.42424242 -2.52525253 -2.62626263 -2.72727273 -2.82828283 -2.92929293 -3.03030303 -3.13131313 -3.23232323 -3.33333333 -3.43434343 -3.53535354 -3.63636364 -3.73737374 -3.83838384 -3.93939394 -4.04040404 -4.14141414 -4.24242424 -4.34343434 -4.44444444]] 地面に衝突したタイミングでシミュレーションが終了していることがわかります。 ground.terminal = Trueの行をコメントアウトしたり、値をFalseに設定するとt=10までシミュレーションが継続します。 参考文献 scipy.integrate.solve_ivp — SciPy v1.6.3 Reference Guide (https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LightGBMをOptunaでチューニング

LightGBMをOptunaでチューニングする 前提条件 Python 3.7 Optunaでチューニング import部分を書き換えるだけ。   これで、KaggleのHouse Pricesコンペの結果が0.13120 -> 0.12557に改善した。 変更前 import lightgbm as lgb 変更後 import optuna.integration.lightgbm as lgb 参考 LightGBMとOptunaを導入・動かしてみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第11回 今更だけど基礎から強化学習を勉強する モンテカルロ木探索編

モデルベース強化学習の有名な手法であるモンテカルロ木探索を見ていきたいと思います。 第10回 モデルベース基礎編 ※ネット上の情報をかき集めて自分なりに実装しているので正確ではない可能性がある点はご注意ください コード全体 本記事で作成したコードは以下です。 GoogleColaboratory モンテカルロ探索(Monte-Carlo Search) まずは木を使わない場合の探索です。 あるモデル $M_\eta$ とシミュレーション方策 $\pi$ が与えられているとします。 ※$M_\eta$ は近似モデルを想定していますが、実環境でも問題ありません ※$\pi$ はシミュレーション時にアクションを選ぶ方策です この時、ある状態 $s_t$ におけるアクションの選択方法は以下です。 q_vals = [] for action in 全アクション: total_reward = 0 for エピソードをK回分繰り返す: state = エピソードを開始する状態はある状態st for 1エピソードループ: エピソード中のアクションは方策πによって選ばれる total_reward += 各stepの報酬 # Kエピソードの平均報酬、これが行動価値になる reward = total_reward / K q_vals.append(reward) # 行動価値が一番高い行動を選択する select_action = np.argmax(q_vals) 方策πは一般的にはランダムな方策が使われます。 その場合はモンテカルロ法で行動価値を求めていると見ることもできるので、これが名前の由来になっているのでしょう。 割引報酬に関する考察 モデルフリーなアルゴリズムでは累積報酬に関して割引率を反映していました。 G^{free}_t = r_{t+1} + \gamma G^{free}_{t+1} しかし、モデルベースなアルゴリズムでは適用していないように見えます。 G^{base}_t = r_{t+1} + G^{base}_{t+1} これはモデルベースでは近似モデルを採用しており、そもそも即時報酬自体が近似値なのでさらに割引して正確性を失う必要がないからではないかと思っています。 (それかモンテカルロ探索だけがそうなのかもしれません) モンテカルロ木探索(Monte-Carlo Tree Search) モンテカルロ探索では行動価値関数を全アクションに対して走査していましたが、最初のアクションもシミュレーション方策 $\pi$ によって決める方法がモンテカルロ木探索です。 参考 強化学習理論の基礎4 強化学習入門 Part3 - AlphaGoZeroでも重要な技術要素! モンテカルロ木探索の入門 - モンテカルロ木探索(Wikipedia) AlphaGo Zeroの動作方法と理由 モンテカルロ木探索はシミュレーション時に得た行動価値関数と訪れた(状態,アクション)の回数を保存します。 この保存した情報を元にシミュレーション方策 $\pi_{tree}$ を更新していきます。 (後述しますがモンテカルロ木探索には2つの方策があります) シミュレーションはSelection,Expansion,Evaluation,Backupのステップがあります。 1. Selection 今いる状態からアクションを選びます。 選び方ですが、各アクションを多腕バンディット問題(Multi-armed bandit problem)として選択します。 多腕バンディット問題を簡単に言うと探索と活用のジレンマを表現した問題です。 当たる確率が分からないスロットがいくつかあります。 スロットは回せば回すほど当たる確率がわかりますが、当たらないスロットを回すと損します。 ここでスロットの本当の確率を予測する行為(探索)と今わかる確率で一番いいスロットを回す行為(活用)にトレードオフが発生し、その中で最大の報酬を得ようとする問題が多腕バンディット問題となります。 多腕バンディット問題に関しては過去に記事を書いているのでよかったらどうぞ 【強化学習】複数の探索ポリシーを実装/解説して比較してみた(多腕バンディット問題) モンテカルロ木探索の Selection には UCB(Upper Confidence Bound)1 がよく使われ、以下の式となります。 $$ \underset{a}{argmax} \biggl( Q(s,a) + \sqrt{\frac{2 logN_s}{n_{s,a}}} \biggr) $$ $Q(s,a)$ は行動価値、$N_s$ は状態$s$を訪れた回数、$n_{s,a}$は状態$s$でアクション$a$を選んだ回数です。 state = 対象の状態 N = stateを訪れた回数 select_val_list = [] for action in 全アクション: n = stateでactionを選んだ回数 if n == 0: # 一度は選んでほしい val = 9999999 else: ucb_cost = np.sqrt(s * np.log(N)) / n val = (state,action)の行動価値 + ucb_cost select_val_list.append(val) select_action = np.argmax(select_val_list) 2. Expansion Selectionで選んだアクションの回数が閾値以上なら次の状態に遷移し、その状態を基準にしてまた Selection に戻ります。 これは状態とアクションを木構造に見立てた場合、葉の要素をさらに展開する感じになります。 閾値以下の場合は Expansion はせずに Evaluation に進みます。 3. Evaluation アクションの選択回数が閾値未満だった場合、その状態の価値を推定します。 評価の仕方は適当な方策 $\pi_d$ でエピソード最後までシミュレーションし、その中で得られた報酬を価値として計算します。 このエピソード最後までシミュレーションして価値を推定する事をロールアウト(Rollout)といいます。 また、モンテカルロ木探索ではシミュレーション内で2つの方策があり、Selection で使われる方策を Tree Policy ($\pi_{tree}$)といい、Evaluation で使われる方策を Default Policy ($\pi_{d}$)といいます。 4. Backup Expansion によって評価された値をシミュレーションで辿った(状態,アクション)の行動価値に反映させます。 反映の仕方に特に決まりはなさそうです。 今回の場合ですと、素直に考えれば累積報酬の期待値でしょうか。 G'_{t,s,a} = r_{t+1} + G'_{t+1,s,a} \\ Q(s,a) = E[G] = \frac{\sum_{i} G'_{i,s,a}}{N_{s,a}} $G_{t,s,a}$ は$(s,a)$からエピソード最後までシミュレーションした結果得た価値、$N_{s,a}$ は$(s,a)$を訪れた回数です。 Play 最後に実際に実行するアクションを決めます。 決め方はシミュレーションの結果得た行動価値(Q値)を使えばいいのですが、他の方法もあるようです。 例えば AlphaGo では選択された回数が一番多いアクションを選択しているそうです。 (Q値より外れ値の影響を受けにくいそうです) 実装のメインフロー(共通) アルゴリズム部分はクラス化し、メインフローはそこだけ変える形で実装しています。 env = GridEnv() a_mdp = A_MDP(env) # 近似モデル(詳細は第10回を参照) rl_mc = 各アルゴリズムのクラスをインスタンス化 # 学習ループ for episode in range(100): state = env.reset() done = False total_reward = 0 # 1episode 最大20stepで終わりにする for step in range(20): # アクションを決定 action = rl_mc.sample_action(a_mdp, state, training=True) # 環境の1step n_state, reward, done, _ = env.step(action) total_reward += reward # 近似モデルの学習 a_mdp.train(state, action, n_state, reward, done) state = n_state if done: break # 5回テストする例 for episode in range(5): state = np.asarray(env.reset()) env.render() done = False total_reward = 0 # 1episode for step in range(20): action = rl_mc.sample_action(a_mdp, state) n_state, reward, done, _ = env.step(action) env.render() state = np.asarray(n_state) total_reward += reward print("{} reward: {}".format(episode, total_reward)) env.close() 実装1,モンテカルロ探索 モンテカルロ探索から実装してみます。 各方策は以下で実装しました。 方策 シミュレーション方策 ランダム 実環境へのアクションの選択方法(トレーニング) ε-Greedy法 実環境へのアクションの選択方法(テスト) 最大のQ値 class MCS(): def __init__(self, env): self.nb_action = len(env.actions) # アクション数 self.max_turn = 20 # 最大ターン数 self.epsilon = 0.1 self.simulation_times = 20 # state から近似モデル a_mdp を元に実環境へのアクションを返す def sample_action(self, a_mdp, state, training=False): # Q値を計算する q_vals = self.compute_q_vals(a_mdp, state) if training: # トレーニング中はε-greedy if np.random.random() < self.epsilon: # epsilonより低いならランダムに移動 return np.random.randint(self.nb_action) else: # Q値が最大のアクションを実行 select_actions = [] for i, v in enumerate(q_vals): if v == max(q_vals): select_actions.append(i) return random.sample(select_actions, 1)[0] else: return np.argmax(q_vals) # 近似モデル a_mdp を使ってQ値を計算する def compute_q_vals(self, a_mdp, init_state): q_vals = [] # 全アクション for action in a_mdp.actions: total_reward = 0 # シミュレーション for _ in range(self.simulation_times): state = a_mdp.sample_next_state(init_state, action) done = a_mdp.sample_done(init_state, action) step = 0 # 1episode while not done and step < self.max_turn: step += 1 # π(ランダム) action = np.random.randint(self.nb_action) total_reward += a_mdp.get_reward(state, action) done = a_mdp.sample_done(state, action) state = a_mdp.sample_next_state(state, action) # Kエピソードの平均報酬、これが行動価値になる reward = total_reward / self.simulation_times q_vals.append(reward) return q_vals 実行結果 学習中の報酬取得の様子 学習後の近似モデルの報酬関数 ------------------------------------------------ -0.04 | -0.04 | 0.07 | 0.00 | -0.04 -0.04|-0.04 -0.04|-0.04 0.85| 0.00 0.00| -0.04 | -0.04 | 0.02 | 0.00 | ------------------------------------------------ -0.04 | 0.00 | -0.13 | 0.00 | -0.04 -0.04| 0.00 0.00|-0.04 -0.87| 0.00 0.00| -0.04 | 0.00 | -0.18 | 0.00 | ------------------------------------------------ -0.04 | -0.04 | -0.04 | -0.76 | -0.04 -0.04|-0.04 -0.04|-0.04 -0.04|-0.12 -0.16| -0.04 | -0.04 | -0.04 | -0.04 | ------------------------------------------------ 学習後の近似モデルの終了関数 ------------------------------------------------------- 0.0% | 0.0% | 10.2% | 0.0% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 85.5%| 0.0% 0.0%| 0.0% | 0.0% | 6.1% | 0.0% | ------------------------------------------------------- 0.0% | 0.0% | 9.1% | 0.0% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 86.0%| 0.0% 0.0%| 0.0% | 0.0% | 14.3% | 0.0% | ------------------------------------------------------- 0.0% | 0.0% | 0.0% | 75.4% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 0.0%| 8.3% 12.0%| 0.0% | 0.0% | 0.0% | 0.0% | ------------------------------------------------------- 学習後の近似モデルの遷移関数 (0, 0):Action.UP -> (0, 0):92.1% (0, 0):Action.UP -> (1, 0):7.9% (0, 0):Action.DOWN -> (0, 0):8.1% (0, 0):Action.DOWN -> (1, 0):10.0% (0, 0):Action.DOWN -> (0, 1):81.9% (0, 0):Action.LEFT -> (0, 0):88.1% (0, 0):Action.LEFT -> (0, 1):11.9% (0, 0):Action.RIGHT -> (0, 0):7.4% (0, 0):Action.RIGHT -> (1, 0):86.4% (0, 0):Action.RIGHT -> (0, 1):6.2% (1, 0):Action.UP -> (0, 0):11.8% (1, 0):Action.UP -> (1, 0):83.5% (1, 0):Action.UP -> (2, 0):4.7% (1, 0):Action.DOWN -> (0, 0):7.5% (1, 0):Action.DOWN -> (1, 0):85.0% (1, 0):Action.DOWN -> (2, 0):7.5% (1, 0):Action.LEFT -> (0, 0):78.3% (1, 0):Action.LEFT -> (1, 0):21.7% (1, 0):Action.RIGHT -> (1, 0):20.4% (1, 0):Action.RIGHT -> (2, 0):79.6% (2, 0):Action.UP -> (1, 0):8.2% (2, 0):Action.UP -> (2, 0):81.6% (2, 0):Action.UP -> (3, 0):10.2% (2, 0):Action.DOWN -> (1, 0):9.1% (2, 0):Action.DOWN -> (3, 0):6.1% (2, 0):Action.DOWN -> (2, 1):84.8% (2, 0):Action.LEFT -> (1, 0):79.1% (2, 0):Action.LEFT -> (2, 0):7.0% (2, 0):Action.LEFT -> (2, 1):14.0% (2, 0):Action.RIGHT -> (2, 0):9.1% (2, 0):Action.RIGHT -> (3, 0):85.5% (2, 0):Action.RIGHT -> (2, 1):5.5% (0, 1):Action.UP -> (0, 0):83.8% (0, 1):Action.UP -> (0, 1):16.2% (0, 1):Action.DOWN -> (0, 1):18.4% (0, 1):Action.DOWN -> (0, 2):81.6% (0, 1):Action.LEFT -> (0, 0):9.8% (0, 1):Action.LEFT -> (0, 1):80.8% (0, 1):Action.LEFT -> (0, 2):9.3% (0, 1):Action.RIGHT -> (0, 0):9.8% (0, 1):Action.RIGHT -> (0, 1):79.9% (0, 1):Action.RIGHT -> (0, 2):10.3% (2, 1):Action.UP -> (2, 0):78.2% (2, 1):Action.UP -> (2, 1):12.7% (2, 1):Action.UP -> (3, 1):9.1% (2, 1):Action.DOWN -> (2, 1):17.9% (2, 1):Action.DOWN -> (3, 1):14.3% (2, 1):Action.DOWN -> (2, 2):67.9% (2, 1):Action.LEFT -> (2, 0):5.3% (2, 1):Action.LEFT -> (2, 1):84.2% (2, 1):Action.LEFT -> (2, 2):10.5% (2, 1):Action.RIGHT -> (2, 0):4.7% (2, 1):Action.RIGHT -> (3, 1):86.0% (2, 1):Action.RIGHT -> (2, 2):9.3% (0, 2):Action.UP -> (0, 1):80.7% (0, 2):Action.UP -> (0, 2):9.6% (0, 2):Action.UP -> (1, 2):9.6% (0, 2):Action.DOWN -> (0, 2):89.7% (0, 2):Action.DOWN -> (1, 2):10.3% (0, 2):Action.LEFT -> (0, 1):11.7% (0, 2):Action.LEFT -> (0, 2):88.3% (0, 2):Action.RIGHT -> (0, 1):8.7% (0, 2):Action.RIGHT -> (0, 2):9.0% (0, 2):Action.RIGHT -> (1, 2):82.3% (1, 2):Action.UP -> (0, 2):9.0% (1, 2):Action.UP -> (1, 2):81.4% (1, 2):Action.UP -> (2, 2):9.6% (1, 2):Action.DOWN -> (0, 2):8.2% (1, 2):Action.DOWN -> (1, 2):78.9% (1, 2):Action.DOWN -> (2, 2):12.9% (1, 2):Action.LEFT -> (0, 2):78.0% (1, 2):Action.LEFT -> (1, 2):22.0% (1, 2):Action.RIGHT -> (1, 2):22.3% (1, 2):Action.RIGHT -> (2, 2):77.7% (2, 2):Action.UP -> (2, 1):82.1% (2, 2):Action.UP -> (1, 2):7.1% (2, 2):Action.UP -> (3, 2):10.7% (2, 2):Action.DOWN -> (1, 2):5.7% (2, 2):Action.DOWN -> (2, 2):87.1% (2, 2):Action.DOWN -> (3, 2):7.1% (2, 2):Action.LEFT -> (2, 1):6.8% (2, 2):Action.LEFT -> (1, 2):84.9% (2, 2):Action.LEFT -> (2, 2):8.2% (2, 2):Action.RIGHT -> (2, 1):12.5% (2, 2):Action.RIGHT -> (2, 2):8.0% (2, 2):Action.RIGHT -> (3, 2):79.5% (3, 2):Action.UP -> (3, 1):75.4% (3, 2):Action.UP -> (2, 2):12.3% (3, 2):Action.UP -> (3, 2):12.3% (3, 2):Action.DOWN -> (2, 2):20.0% (3, 2):Action.DOWN -> (3, 2):80.0% (3, 2):Action.LEFT -> (3, 1):8.3% (3, 2):Action.LEFT -> (2, 2):83.3% (3, 2):Action.LEFT -> (3, 2):8.3% (3, 2):Action.RIGHT -> (3, 1):12.0% (3, 2):Action.RIGHT -> (3, 2):88.0% 学習後のQ値(モンテカルロ探索) ------------------------------------------------ -0.46 | -0.04 | -0.19 | -0.47 | -0.62 -0.68|-0.28 -0.45|-0.11 -0.05|-0.50 -0.33| -0.70 | 0.01 | -0.14 | -0.75 | ------------------------------------------------ -0.35 | -0.59 | -0.15 | -0.61 | -0.86 -0.63|-0.38 -0.36|-0.21 -0.14|-0.45 -0.71| -0.68 | -0.32 | -0.19 | -0.28 | ------------------------------------------------ -0.60 | -0.86 | -0.87 | -0.22 | -1.11 -0.84|-0.82 -1.14|-0.89 -0.85|-0.17 -0.28| -0.77 | -0.82 | -0.85 | -0.29 | ------------------------------------------------ 学習後、テスト100の結果 実装2,モンテカルロ木探索 各方策とコードは以下です。 方策 シミュレーション方策(Default Policy) ランダム シミュレーション方策(Tree Policy) UCB1 実環境へのアクションの選択方法(トレーニング) UCB1(Tree Policyと同じ) 実環境へのアクションの選択方法(テスト) 最大のQ値 class MCTS(): def __init__(self, env): self.nb_action = len(env.actions) self.max_turn = 20 self.nb_action = len(env.actions) # アクション数 self.max_turn = 20 # 最大ターン数 self.count_threshold = 100 # Expansionの閾値 self.simulation_times = 10 # シミュレーション回数 self.Q = collections.defaultdict(lambda: [0] * self.nb_action) self.count = collections.defaultdict(lambda: [0] * self.nb_action) # state から近似モデル a_mdp を元に実環境へのアクションを返す def sample_action(self, a_mdp, state, training=False): if training: # 数回シミュレーションする for _ in range(self.simulation_times): self._simulation(a_mdp, state) # UCBに従ってアクションを選択 return self._select_action(a_mdp, state) else: # テスト中はQ値が最大のアクションを選択 q = [] for action in a_mdp.actions: if self.count[state][action] == 0: q.append(0) else: q.append(self.Q[state][action] / self.count[state][action]) return np.argmax(q) # UCBに従ってアクションを返す def _select_action(self, a_mdp, state): # この状態をシミュレートした回数 N = np.sum(self.count[state]) # selection ucb_list = [] for action in a_mdp.actions: # この(状態,アクション)をシミュレートした回数 n = self.count[state][action] if n == 0: # 0回は1度は選んでほしい ucb = 99999999 else: # UCB値を計算 cost = np.sqrt(2 * np.log(N)) / n ucb = self.Q[state][action] / n + cost ucb_list.append(ucb) # UCBが最大のアクションを選ぶ(複数ある場合はその中からランダム) select_actions = [] for i, v in enumerate(ucb_list): if v == np.max(ucb_list): select_actions.append(i) return random.sample(select_actions, 1)[0] # シミュレーションして行動価値Qと選択回数を更新する # 再帰関数として実装し、戻り値は報酬の配列(逆順) def _simulation(self, a_mdp, state, depth=0): if depth >= self.max_turn: return [] # UCBに従ってアクションを選択 action = self._select_action(a_mdp, state) # 回数を増やす self.count[state][action] += 1 # 報酬 reward = a_mdp.get_reward(state, action) if self.count[state][action] < self.count_threshold: # 閾値以下はロールアウト reverse_reward_list = self._rollout(a_mdp, state, action) reverse_reward_list.append(reward) else: done = a_mdp.sample_done(state, action) if done: # 終了なら展開しない reverse_reward_list = [reward] else: # 展開して次の状態に n_state = a_mdp.sample_next_state(state, action) reverse_reward_list = self._simulation(a_mdp, n_state, depth+1) reverse_reward_list.append(reward) # Q値の更新 self.Q[state][action] += np.sum(reverse_reward_list) return reverse_reward_list # ロールアウト def _rollout(self, a_mdp, state, action): step = 1 done = a_mdp.sample_done(state, action) state = a_mdp.sample_next_state(state, action) reward_list = [a_mdp.get_reward(state, action)] # 1episode while not done and step < self.max_turn: step += 1 # π(ランダム) action = random.randint(0, len(a_mdp.actions)-1) reward = a_mdp.get_reward(state, action) reward_list.append(reward) done = a_mdp.sample_done(state, action) state = a_mdp.sample_next_state(state, action) return list(reversed(reward_list)) 実行結果 学習中の報酬取得の様子 学習後の近似モデルの報酬関数 ------------------------------------------------ -0.04 | -0.04 | -0.04 | 0.00 | 0.00 -0.04| 0.00 -0.04| 0.00 0.85| 0.00 0.00| -0.04 | 0.00 | -0.04 | 0.00 | ------------------------------------------------ -0.04 | 0.00 | -0.04 | 0.00 | 0.00 -0.04| 0.00 0.00| 0.00 -0.68| 0.00 0.00| -0.04 | 0.00 | -0.04 | 0.00 | ------------------------------------------------ -0.04 | -0.04 | -0.04 | -0.52 | -0.04 -0.04|-0.04 -0.04|-0.04 -0.04|-1.00 -0.04| -0.04 | -0.04 | -0.04 | -0.04 | ------------------------------------------------ 学習後の近似モデルの終了関数 ------------------------------------------------------- 0.0% | 0.0% | 0.0% | 0.0% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 85.3%| 0.0% 0.0%| 0.0% | 0.0% | 0.0% | 0.0% | ------------------------------------------------------- 0.0% | 0.0% | 0.0% | 0.0% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 66.7%| 0.0% 0.0%| 0.0% | 0.0% | 0.0% | 0.0% | ------------------------------------------------------- 0.0% | 0.0% | 0.0% | 50.0% | 0.0% 0.0%| 0.0% 0.0%| 0.0% 0.0%|100.0% 0.0%| 0.0% | 0.0% | 0.0% | 0.0% | ------------------------------------------------------- 学習後の近似モデルの遷移関数 (0, 0):Action.UP -> (0, 0):100.0% (0, 0):Action.DOWN -> (0, 1):100.0% (0, 0):Action.RIGHT -> (0, 0):5.8% (0, 0):Action.RIGHT -> (1, 0):79.3% (0, 0):Action.RIGHT -> (0, 1):14.9% (1, 0):Action.UP -> (1, 0):100.0% (1, 0):Action.RIGHT -> (1, 0):20.7% (1, 0):Action.RIGHT -> (2, 0):79.3% (2, 0):Action.UP -> (2, 0):100.0% (2, 0):Action.DOWN -> (2, 1):100.0% (2, 0):Action.RIGHT -> (2, 0):3.7% (2, 0):Action.RIGHT -> (3, 0):85.3% (2, 0):Action.RIGHT -> (2, 1):11.0% (0, 1):Action.UP -> (0, 0):81.0% (0, 1):Action.UP -> (0, 1):19.0% (0, 1):Action.DOWN -> (0, 2):100.0% (0, 1):Action.RIGHT -> (0, 1):100.0% (2, 1):Action.UP -> (2, 0):87.5% (2, 1):Action.UP -> (2, 1):12.5% (2, 1):Action.DOWN -> (2, 1):33.3% (2, 1):Action.DOWN -> (2, 2):66.7% (2, 1):Action.RIGHT -> (3, 1):66.7% (2, 1):Action.RIGHT -> (2, 2):33.3% (0, 2):Action.UP -> (0, 1):79.5% (0, 2):Action.UP -> (0, 2):12.3% (0, 2):Action.UP -> (1, 2):8.2% (0, 2):Action.DOWN -> (0, 2):80.0% (0, 2):Action.DOWN -> (1, 2):20.0% (0, 2):Action.LEFT -> (0, 2):100.0% (0, 2):Action.RIGHT -> (0, 1):16.7% (0, 2):Action.RIGHT -> (1, 2):83.3% (1, 2):Action.UP -> (0, 2):37.5% (1, 2):Action.UP -> (1, 2):50.0% (1, 2):Action.UP -> (2, 2):12.5% (1, 2):Action.DOWN -> (0, 2):28.6% (1, 2):Action.DOWN -> (1, 2):71.4% (1, 2):Action.LEFT -> (0, 2):61.5% (1, 2):Action.LEFT -> (1, 2):38.5% (1, 2):Action.RIGHT -> (1, 2):16.7% (1, 2):Action.RIGHT -> (2, 2):83.3% (2, 2):Action.UP -> (2, 1):75.0% (2, 2):Action.UP -> (3, 2):25.0% (2, 2):Action.DOWN -> (2, 2):100.0% (2, 2):Action.LEFT -> (1, 2):100.0% (2, 2):Action.RIGHT -> (3, 2):100.0% (3, 2):Action.UP -> (3, 1):50.0% (3, 2):Action.UP -> (2, 2):50.0% (3, 2):Action.DOWN -> (3, 2):100.0% (3, 2):Action.LEFT -> (3, 1):100.0% (3, 2):Action.RIGHT -> (3, 2):100.0% シミュレーション回数 ----------------------------------------------------------- 66 | 5 | 22 | 9 | 4 4574| 3 5697| 17 5863| 3 590| 15 | 5 | 20 | 20 | ----------------------------------------------------------- 4002 | 0 | 252 | 2 | 4 18| 0 0| 18 33| 1 2| 15 | 0 | 18 | 2 | ----------------------------------------------------------- 1431 | 96 | 34 | 137 | 55 62| 317 76| 27 53| 90 53| 54 | 59 | 26 | 46 | ----------------------------------------------------------- 行動価値 ---------------------------------------------------- -0.46 | -0.90 | -0.39 | -1.04 | -1.33 0.70|-1.33 0.78|-0.48 0.83|-2.04 -0.49| -0.66 | -0.93 | -0.40 | -0.68 | ---------------------------------------------------- 0.64 | 0.00 | 0.33 | -1.02 | -1.53 -0.72| 0.00 0.00|-0.60 -0.50|-2.00 -0.54| -0.77 | 0.00 | -0.62 | -1.14 | ---------------------------------------------------- 0.47 | -0.78 | -0.52 | -0.87 | -0.66 -0.66| 0.12 -0.79|-0.55 -0.51|-0.88 -0.90| -0.67 | -0.80 | -0.55 | -0.94 | ---------------------------------------------------- コスト ---------------------------------------------------- 0.06 | 0.83 | 0.19 | 0.40 | 1.03 0.00| 1.39 0.00| 0.25 0.00| 1.20 0.01| 0.27 | 0.83 | 0.21 | 0.18 | ---------------------------------------------------- 0.00 | 0.00 | 0.01 | 0.99 | 1.02 0.23| 0.00 0.00| 0.19 0.10| 1.97 0.99| 0.27 | 0.00 | 0.19 | 0.99 | ---------------------------------------------------- 0.00 | 0.04 | 0.09 | 0.02 | 0.07 0.06| 0.01 0.05| 0.12 0.06| 0.04 0.06| 0.07 | 0.06 | 0.12 | 0.07 | ---------------------------------------------------- 行動価値+コスト(UCB値) ---------------------------------------------------- -0.40 | -0.07 | -0.21 | -0.65 | -0.30 0.70| 0.05 0.78|-0.24 0.83|-0.84 -0.48| -0.39 | -0.10 | -0.19 | -0.50 | ---------------------------------------------------- 0.64 | 0.00 | 0.34 | -0.03 | -0.51 -0.49| 0.00 0.00|-0.41 -0.40|-0.03 0.45| -0.50 | 0.00 | -0.43 | -0.15 | ---------------------------------------------------- 0.47 | -0.75 | -0.43 | -0.84 | -0.59 -0.60| 0.13 -0.74|-0.43 -0.45|-0.84 -0.84| -0.60 | -0.74 | -0.43 | -0.86 | ---------------------------------------------------- 学習後、テスト100の結果 実行結果はかなりいい結果の場合です。 ロールアウトで-1の報酬ばかり引くとそれが伝搬して学習が失敗します。(局所解に入ります) あとがき 乱数による影響が大きく学習はあまり安定しませんでした。 次はテーブル型ではなく関数近似したモデルを扱いたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】netkeibaのスクレイピングで必要なrace_idを実在するものだけ集める

はじめに netkeibaをスクレイピングするにあたってrace_idというものがキーになります. このrace_idを実在するレースのものだけ集める方法を載せています. 私の性格的にデータの存在しないURLにもアクセスながらデータを集めるという作業が気に入らなくて、まずは実在するrace_idだけを集めたかったという極めて非効率的なお話なので実用的ではないです. ただこの手法でrace_idを集めておけば今後またnetkeibaをスクレイピングしたいというときに無駄なアクセスを減らすという意味で役に立ちます. 1回のスクレイピングで必要な情報全て集めておけよという話ですが. race_idとは netkeibaで過去のレース情報が載せられたデータベースのURLはhttps://db.netkeiba.com/race/ + "race_id"という構成になっています. race_idは12桁の数字で構成されており, "開催年4桁" + "開催地を表す2桁" + "何回目かを表す2桁" + "何日目かを表す2桁" + "何レース目かを表す2桁"という内訳です. 例えば2021年中山競馬場第3回8日目11R皐月賞のrace_idは"202106030811"となります. 実際のデータベース → https://db.netkeiba.com/race/202106030811/ 開催年、開催地、何レース目というのはほぼ固定された範囲に収まりますが、何回開催されるか、何日開催されるかというのはその年や開催地によって異なるため、適当にrange決めてfor文回すだけでは無駄なアクセスが発生してしまいます. 仮に4回目のデータが存在しなければ5回目のデータも当然存在せず、7日目のデータが存在しなければ8日目も当然存在しないため、そうしたrace_idを上手く飛ばしながらデータが存在するrace_idだけを集めます. 実行環境 Windows10 Python-3.9.4 コード コード全文以下に載せます. 主に参考[2],参考[3]の記事を元に書いています. ※コード書き始めて1,2か月の初心者がとりあえず目的通りに動くことを確認したスマートではないコードであることをお許しください. 注意事項 time.sleep(n) は必ずコードとして入れてください. n秒の遅延を発生させることができます. これがないと高頻度でアクセスすることとなりwebサイトへ負担となります. 私も詳しいことはわかりませんが罪になりかねません. コード全文 import requests from bs4 import BeautifulSoup import pickle import time #sleep用 # 取得race_id保存&ロード用 ※参考[1] def pickle_dump(obj, path): with open(path, mode='wb') as f: pickle.dump(obj,f) def pickle_load(path): with open(path, mode='rb') as f: data = pickle.load(f) return data # 2006~2021年までのrace_idを集める. 2005年以前は何故かエラーで弾かれる... for year in range(2006,2021,1): race_id_list = [] year_str = str(year).zfill(4) for place in range(1,11): place_str = str(place).zfill(2) race_error = 0 kai_error = 0 day_error = 0 for kai in range(1,11): kai_str = str(kai).zfill(2) if kai_error == 1: break for day in range(1,30): if day_error == 1 or kai_error == 1: day_error = 2 break day_str = str(day).zfill(2) for race in range(1,13): time.sleep(2) race_str = str(race).zfill(2) race_id = year_str + place_str + kai_str + day_str + race_str sitename = "https://db.netkeiba.com/race/" + race_id html = requests.get(sitename) html.encoding = "EUC-JP" soup = BeautifulSoup(html.text, "html.parser") title_text = soup.find('title').get_text() #データがない場合通常"YYYY年MM月DD日"と書かれるタイトルテキストに"年月日"が出てくる #という点を根拠にしたデータあるなしチェックです. if '年月日' in title_text: if race_error == 1: day_error = 1 race_error = 0 break elif day_error == 2: kai_error = 1 day_error = 0 break else: race_error = 1 break else: race_id_list.append(race_id) race_error = 0 kai_error = 0 day_error = 0 print(race_id,title_text) #保存先の指定. filename = './race_id_list/' + year_str + '_race_id_list.pickle' #データの保存 pickle_dump(race_id_list, filename) 参考 pickleを用いたファイルの保存はこちらを参考にさせていただきました. 参考1:【python】リストや辞書を外部ファイルに保存 スクレイピングに関しては以下のサイトを参考にさせていただきました. 参考2:【Python】スクレイピングで競馬のデータを集める【予想準備】 参考3: スクレイピング|競馬予想で始める機械学習〜完全版〜
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SenseHAT でスペクトログラム表示

今までと趣向を変えて、今回は音声を扱ってみました。思いついたのが、瞬間ごとの周波数分布を表示するスペクトログラムを、SenseHATの画面で出してみるというものです。結果が正しいかちょっと怪しいですが…それっぽくは出来たかな? なお、今回RaspberryPiは表示のみで、画面として使いました。フーリエ変換などはPC側でやっており、その結果(単位時間ごとの周波数分布の配列)をWebSocketでRaspberryPiへ送ります。なぜかというと、RaspberryPiにlibrosaという音声系ライブラリのインストールが出来なかっただけです。。が、今となってはこの表示と処理を分けた構成もアリかなと思えます。 参考:https://jorublog.site/python-voice-analysis/ それにしても、scipyは初めて使いましたが、便利ですねー。 以下は、PC側で実行した解析処理のコードです。最終的に、変数amp_msgをWebSocketでRaspberryPiへ送ります。 import scipy.io.wavfile import numpy as np import librosa def analyze(filename, timespan=0.001, timerate=0.1): rate, data = scipy.io.wavfile.read(filename) data = data / 32768 time = np.arange(0, data.shape[0]/rate, 1/rate) fft_data = np.abs(np.fft.fft(data)) freqList = np.fft.fftfreq(data.shape[0], d=1.0/rate) fft_size = 1024 hop_length = int(fft_size / 4) cols = 8 dBMax = 100 dBMin = -100 tpitch = int(rate*timespan) timecount = int(len(data)/tpitch) timecount = int(timecount*timerate) amp_msg = list() for f in range(timecount): tstt = f*tpitch tend = tstt + tpitch amplitude = np.abs(librosa.core.stft(data[tstt:tend], n_fft=fft_size, hop_length=hop_length)) log_power = librosa.core.amplitude_to_db(amplitude) fpitch = int(log_power.size/8) amp = np.ndarray(cols) msg = '' for p in range(cols): stt = fpitch*p end = stt + fpitch freq = np.max(log_power[stt:end]) freq8 = int(np.ceil( (freq-(dBMin))/(dBMax-dBMin)*cols )) amp[p] = freq8 amp_msg.append( str(int(amp[0])) + ',' + str(int(amp[1])) + ',' + str(int(amp[2])) + ',' + str(int(amp[3])) + ',' + str(int(amp[4])) + ',' + str(int(amp[5])) + ',' + str(int(amp[6])) + ',' + str(int(amp[7])) ) return amp_msg
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonによる手取り計算

概要 月ごとの手取り額をpythonで簡単に概算できるようにしました。 結果は以下の通りです。 はじめに 新卒でお給料をもらったものの、実際には額面から色々と引かれることを実感したこの頃です。 そこで、 - どのくらいのお給料が税金や社会保障費となっているのか - 今後額面が増えると仮定して、実際にどの程度手元に残るのか を可視化しました。 仕様等 実装はこちらをご覧ください。 環境:Python 3.7.4 入力変数:額面, 年齢, 都道府県, 交通費, 残業代, 扶養人数 出力:手取り、控除費用(グラフ順に健康保険費用、介護保険費用、厚生年金、雇用保険、所得税、住民税) おわりに 表形式で羅列されたものは存在します。しかし、より詳細かつ直感的にグラフ化されたものは見つからなかったので、自分で計算を実装し可視化することにしました。 以下のサイトを計算の参考に用いました。 https://funjob.jp/keisan/gekkyu/ 実装はかなり地道ですが、勉強になったので良しとします。 注意 ボーナス等は考慮しません。 住民税は前年の収入に対してかかるので、正確ではありません。 また、原点付近の挙動も正しくありません。 参考までにお使いください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kivyMDチュートリアル其の弍什 Components - Label篇

みなさん、おはこんばんは。いかが、お過ごしでしょうか。 暑くなり少しジメジメ感が漂ってきて、夏の到来が感じてくるようになりましたね。 避暑したい気持ちがマンマンです。 今週はというと、またGitHubが障害を起こしていたようですね。そのときは、触れては いなかったのですが今週末だとバッドエンドでした(そこまでではない)。でも、安心して ください(なにが)。GitHubがなくならない以上は、KivyMDのお時間はなくなりません( 当たり前)。サービスエンドしたときは、GitLabに移行しようかな(しらないよ)。 とまぁ、グダグダ続けてきたわけなのですが、なんとComponentsの半分を差し掛かるよう になりました!これも皆さんのおかげです。なんか早いですね、1年くらい掛かるかなぁと 思っていたのに。GW特別企画頑張ったからかな。でもまぁ、気を緩まず精進していこうと 思います。お付き合い頂ければ本望です。 とまぁ、前置きが長くなってきたのでさっそく今週も元気にやっていきましょう! 今日は、とても大事なLabel篇となります。 Label はい、今日はめずらしく「See also」パネルにあるリンクがありません。 最初に概要が記載されています。 The MDLabel widget is for rendering text. で、謎に今日のお題目でもある以下のコンポーネントらが列挙されています。 ● MDLabel ● MDIcon 図々しくもMDIconがくっ付いてきていますね。なぜなのでしょうか。このことについては、 後々触れることにします。 MDLabel ここでも概要が記載されています。 Class MDLabel inherited from the Label class but for MDLabel the text_size parameter is (self.width, None) and default is positioned on the left: MDLabelはkivyのLabelを継承していますが、text_sizeパラメータは上記の通りであり、 デフォルトは左寄りで配置されるとのことです。 で、さっそくコードに入っていきます。今日はコードの参照が多いので、修正量を 少なめにして結果を即座に映すことにします。 xx/mdlabel.py from kivy.lang import Builder from kivymd.app import MDApp KV = ''' Screen: BoxLayout: orientation: "vertical" MDToolbar: title: "MDLabel" MDLabel: text: "MDLabel" # add MDLabel: text: "MDLabel" halign: "center" ''' class Test(MDApp): def build(self): return Builder.load_string(KV) Test().run() マニュアルでは、デフォルトとセンター配置しているMDLabelの定義がありましたので 上記では両方の挙動を見ています。追加文はコメントアウトしているところあたりになります。 さっそく修正しているわけですが。。 これといって、目新しいものはないので、初めてですが触れ込みはなしとすることにします。 ですが、「Note」と題したパネルでは以下のように記載があります。 See halign and valign attributes of the Label class このプロパティはkivyMDのAPIあたりに書かれているものではなく、kivy側のLabel クラスにあるプロパティのようですね。探すのに少し苦労しました。とまぁどうでもよい ことは置いておいて、v・hとありピンときた方もいられるかもですが左右上下に配置できる よーというものですね。気になった方は以下をご参照ください。 結果 さくさく結果まできてしまいましたね。どんな挙動になったか見てみましょう。 ちゃんと、マニュアル通りに動いています。デフォルトは左に寄っていることが分かります。 MDLabel color: こちらも概要として以下が記載されています。 MDLabel provides standard color themes for label color management: ここは自信がなかったので、依頼をしてみました。 MDLabelは、ラベルの色を管理するための標準的なカラーテーマを提供します。 とのことです。このこともコードで見てみましょう。 xx/mdlabel_color.py from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.label import MDLabel KV = ''' Screen: BoxLayout: id: box orientation: "vertical" MDToolbar: title: "MDLabel" # add MDLabel: text: "Custom color" halign: "center" theme_text_color: "Custom" text_color: 0, 0, 1, 1 ''' class Test(MDApp): def build(self): screen = Builder.load_string(KV) # Names of standard color themes. for name_theme in [ "Primary", "Secondary", "Hint", "Error", "ContrastParentBackground", ]: screen.ids.box.add_widget( MDLabel( text=name_theme, halign="center", theme_text_color=name_theme, ) ) return screen Test().run() ここもコードを少しアレンジしています。カスタムで色変えれんよ(られるよ)という以下の 記載があるので合わせてそちらも見ています。同様、コードの方はコメントアウトあたりを 追加しているのみです。 To use a custom color for MDLabel, use a theme ‘Custom’. After that, you can specify the desired color in the rgba format in the text_color parameter: ここも、とりとめがないのですが、触れるとするならば以下Testクラスのbuildメソッドに なるでしょうか。一旦追加分(コメントアウト周辺部分)はマニュアル通りなので省略します。 (略) class Test(MDApp): def build(self): screen = Builder.load_string(KV) # Names of standard color themes. for name_theme in [ "Primary", "Secondary", "Hint", "Error", "ContrastParentBackground", ]: screen.ids.box.add_widget( MDLabel( text=name_theme, halign="center", theme_text_color=name_theme, ) ) return screen (略) テキストで表示されるもろもろは色が決まっているということなのですね。これも次の実行 結果を見てみましょう。 結果 いや、さくさく進んでいますね。気持ちがいい。 おっと、肝心なことを忘れてはいけません。結果に入ります。 ここも追加分を合わせてもマニュアル通りですね。試してはないですが、ダークテーマにすると とても見づらいものもありますね。両方ともに適合するテーマカラーはなんだろうかと考えて みるのもデザインの登竜門になりますでしょうか(多分ない)。 font_style parameter ここも章立てしているわけではありませんが、分かりやすくするためにここでは章立てる ことにしました。また、概要としては以下のように記載があります。 MDLabel provides standard font styles for labels. To do this, specify the name of the desired style in the font_style parameter: こちらに関しては、最初あたりにやりましたね。なつかしい。詳しくは以下をご覧ください。 内容が見づらいことはお詫び申し上げということになりますが、フォントのスタイルはマテリ アルデザイン同様でkivyMDでも決まっています。リンクを見ていなく、なんのことじゃいって なっている方はコードと実行結果を見れば一目瞭然となると思われます。 from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.label import MDLabel from kivymd.font_definitions import theme_font_styles KV = ''' Screen: BoxLayout: orientation: "vertical" MDToolbar: title: "MDLabel" ScrollView: MDList: id: box ''' class Test(MDApp): def build(self): screen = Builder.load_string(KV) # Names of standard font styles. for name_style in theme_font_styles[:-1]: screen.ids.box.add_widget( MDLabel( text=f"{name_style} style", halign="center", font_style=name_style, ) ) return screen Test().run() 上記のコードは、マニュアル同様で特に変更点はありません。こちらも先述リンクの 「Font Definitions」篇を見ていた方は周知の事実ですが、何も触れないのは少し おサボりな気がしてならないので少し触れ込んでみます。 import文 Builder・MDAppパッケージは恒例のごとく表れていますが、Testクラス側での呼び 出しにMDLabel及びtheme_font_stylesパッケージを使用する場合は以下のように 指定する必要があります。kvで完結する場合は必要がありません。 from kivymd.uix.label import MDLabel from kivymd.font_definitions import theme_font_styles Testクラス側 ここも先述リンクのおさらいですが、theme_font_stylesはH1だとかH2だとかの フォントのスタイルである文字列を格納しているリストになります。それを終了イン デックスが-1と指定されているのでH1からOverlineを取り出すことになり、1つ1つ MDLabelに流し込んで、idがboxのMDListにガコーンガコーンと詰め込んでいます。 class Test(MDApp): def build(self): screen = Builder.load_string(KV) # Names of standard font styles. for name_style in theme_font_styles[:-1]: screen.ids.box.add_widget( MDLabel( text=f"{name_style} style", halign="center", font_style=name_style, ) ) return screen textプロパティとfont_styleプロパティに取り出したものが指定されていますね。 これでテキスト表示とフォントのスタイルは問題ないようすです。 結果 はい、さっそくですが結果に入ります。 マニュアル通りで少しつまらないのは申し訳ないですが... ってあれッッッ!!!表示狂ってるやんけ?!(突然の方言) 最初表示したときはウイルスにでも感染したか?とかバカなことを思ってたのですが 少し(というか結構な時間つぶして)調査をしてははーん問題ないのだなと確信しました。 確信という強い表現を使ったのは、次の結果を見てもらうと一目瞭然な気がします。 でしょ?!だから私言いましたよね!!(一部界隈でよく使われる文言) すみません、調子に乗りました。。 特別大掛かりなことをやったわけでもありません。どんなことをやったかということを コードで示しておきます。 xx/mdlabel_fontstyle.py (略) MDList: id: box + padding: dp(50) + spacing: dp(100) ''' class Test(MDApp): (略) とりわけ重要なのはspacingの方になりますね。これが大きく設定されていないことで 先程のようなグチャっとした表示になったわけなのです。マニュアルを執筆したときは MDListのデフォルトの仕様が異なっていたのか、spacingなどをサンプルに書き忘れた のか真相のところは定かではありませんが起こっていることは事実です。ちなみにですが、 paddingは上のMDToolbarとの間隔になります。 まぁ、これを見てもフガフガ言わないようにしましょう。ミスは誰にでも起こるものです。 むしろ、コントリビューターが私たちに試練を与えて頂いているというスーパーポジティブ シンキングで物事を考えると心が穏やかになるものです。 # 誰目線で言ってんだと言われそうですが ということで一件落着になりますかね。続いて本日最後のお題目、MDIconになります。 MDIcon えぇ、お前ついてくんのと少しお互い気まずい雰囲気が出ている気がしますが、これには 理由がありそうです。まずは冒頭文の引用から。 You can use labels to display material design icons using the MDIcon class. 高校英語くらい(この文法はまさか...中学...?)文法って大事ですね。。 もうすっかり忘れてしまっていて直訳できないくらいになりました。TOEICやり直そうかな。。 # TOEICの綴りも忘れそうだったなんて恥ずかしくて言えないッ! という自己申告はどうでもよくて、DeepLに依頼するとと以下のようになります。 MDIconクラスを使用して、マテリアルデザインアイコンを 表示するラベルを使用することができます。 少し微妙な気がして、でも使役の〜させるを当てはめてもおかしいしで。。 ということで、私の訳も載せておきます。いらないかなと思いましたけど。。 MDIconクラスを使用してマテリアルデザインのアイコンを 表示するためにラベルを使用することができます。 # 間違ってたら指摘プリーズ! んで、そのあとはここで正体を表したか!といつものマテリアルデザインのリンクが載って ありますが、いつものごとくここではパスします。 続いて、ここで先程の理由がはっきりと分かる案内が書かれています。 The MDIcon class is inherited from MDLabel and has the same parameters. ははーんと分かれば今日はこれでおしまい!となるくらいの1文ですね。なんと、 MDIconクラスはMDLabelを継承しているのでした。んで以下を除くパラメータ は同じものを保持しているとのことです。 Warning For the MDIcon class, you cannot use text and font_style options! MDLabel継承する必要あった?と言いたくなる気持ちもありますが、ここで忘れてはいけ ないことがあります。そう、MDLabelはLabelを継承しています。つまり、LabelはMDIcon のおじいちゃんにあたるので、おじいちゃんのプロパティを使える(意味不明)ので必要は... あるのか...?とまぁ、ここあたりはなにかしらの意図がありそうなったのでしょう!でなけ れば次に進めないw ということで、ここでもコードと実行結果を見てみないと分からないので次のようなものを用意 してみました。まずはコードから。 xx/ from kivy.lang import Builder from kivymd.app import MDApp from kivymd.uix.label import MDIcon from kivymd.icon_definitions import md_icons KV = ''' Screen: BoxLayout: orientation: "vertical" MDToolbar: title: "MDIcon" ScrollView: MDList: id: box padding: dp(20) spacing: dp(30) ''' class Test(MDApp): def build(self): screen = Builder.load_string(KV) # Names of standard font styles. for name_icon in md_icons.keys(): screen.ids.box.add_widget( MDIcon( halign="center", icon=name_icon, ) ) return screen Test().run() サンプルに載ってなくどんなものが来るんだ!と期待感があった方は、すみません。 先程のmdlabel_fontstyle.pyとほとんど同じです。マニュアルでもMDIconの 使い方は載っているし、コードは本当に先程とそっくりそのままなので触れ込みは 割愛させてもらいます。 ざっと説明すればmd_icon・MDIconパッケージを呼び出してアイコンを全て表示して いるだけのものになります。MDIconってなんなんだよ〜!!という方はご安心を。上位 互換なサンプルはすでに用意しています。というか過去に「Icon Definitions篇」で やっていますね。少しこちらはちゃんと(コントリビューターたちが)作っているコード なので少し難しいですが。なので気になる方は以下をご参照ください。 結果 今日も長くなってしまった。。どうでもよいことは置いておいて結果の方にうつります。 これらも問題はなさそうですね。シンプルにアイコンが表示されているだけになります。 ※ 実行する場合は少し重いのでお気をつけを API - kivymd.uix.label 今日使ったAPI群を引用しておきます。 class kivymd.uix.label.MDLabel(**kwargs) もっと情報が欲しいか?、欲しけりゃくれてやるぜ... 探してみろ、コードの中においてきた!と〇〇ピースのような 誰かのセリフみたいに説明があります。module documentationってコードで 合ってんのかな... font_style Label font style. Available vanilla font_style are: ‘H1’, ‘H2’, ‘H3’, ‘H4’, ‘H5’, ‘H6’, ‘Subtitle1’, ‘Subtitle2’, ‘Body1’, ‘Body2’, ‘Button’, ‘Caption’, ‘Overline’, ‘Icon’. font_style is an StringProperty and defaults to ‘Body1’. デフォルトはBody1なんですね。知らなかった。 あと、バニラ フォントスタイルってなんだ...? text Text of the label. 言うまでもなくですね。 theme_text_color Label color scheme name. Available options are: ‘Primary’, ‘Secondary’, ‘Hint’, ‘Error’, ‘Custom’, ‘ContrastParentBackground’. theme_text_color is an OptionProperty and defaults to None. こちらも同様。 text_color Label text color in rgba format. text_color is an ColorProperty and defaults to None. こちらはカスタムでの指定で使用しましたね。 class kivymd.uix.label.MDIcon(**kwargs) こちらも先程と同様、詳細は別のところに置いてきたッ...!というものですね。 icon Label icon name. icon is an StringProperty and defaults to ‘android’. デフォルトはアンドロイドのアイコンです。確かどこかでやった記憶が。。 source Path to icon. source is an StringProperty and defaults to None. 自分でアイコンを用意する場合はこちらを使います。 まとめ さぁ、いかがだったでしょうか。 ラベルが少しでも分かったよーとなってもらえれば嬉しいです。結構重要な項目なので 使いまわせるくらいになりたいものです。分からん!となった方は色々実際に試して もらった方がいいかもしれません。 ちょっと、短めで済ませようと思いましたが長くなってしまいました。 でも、マニュアルと同等の量があるので参考になれば幸いです。 来週は順番通りでList篇になります。今日は長くなってきたのでこの辺で。 それでは、ごきげんよう。 参照 Components » Label https://kivymd.readthedocs.io/en/latest/components/label/ DeepL翻訳/DeepL Translate https://www.deepl.com/ja/translator
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

App Engine(GAE)でサービスごとに複数ドメインを使用する方法

GAEのサービス GAEには「サービス」という概念があって、アプリをデプロイする際にサービス名を指定することで、複数のアプリを1つのプロジェクトで同時に立ち上げることができます。 また、各サービスにドメインを割り当てることもできるので、複数のアプリにそれぞれ異なった複数のドメインを割り当てることも可能です。 app.yamlでサービスを設定する 最初に、アプリごと別のサービスとして立ち上げるためにはapp.yamlにサービス名を指定する必要があります。 app.yaml service: SERVICE_NAME runtime: nodejs10 # ... これをデプロイすればGAEにサービスが立ち上がります。(コンソールのGAE⇨サービスで確認可能) dispatch.yamlでルーティングルールを変更する ここからようやく本題ですが、 dispatch.yamlを使ってドメインんとサービスを紐づけます。 dispatch.yaml dispatch: - url: "*www.example.com/*" service: default # ターゲットとなるサービス名 - url: "*app.example.com/*" service: backend # ターゲットとなるサービス名 この設定は1つのプロジェクト内で共通の設定とされるため、この場合だと、defaultかbackendどちらか1つのディレクトリからデプロイしておけば正しく動作します。 あとは、DNSでそれぞれのドメインをGAEサーバに向ければ完了です。 デメリット GAEで複数のサービスを立ち上げるのはとても便利ですが、その分注意しなければならないデメリットもあります。 コストの最適化のためにも以下の点に注意して適宜Cloud Functionを利用するなり他の選択肢を取ることも考慮すべきです。 サービスの分だけインスタンスが立ち上がる。 価格が高くなりがち
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのコードフォーマッターについての個人的ベストプラクティス

Pythonには多くのlinter・formatterが存在していて、どれを使えばいいか迷います。そこで有名どころをいくつか試してみて、個人的に運用がしやすいと思った組み合わせを考えました。 linterとformatter linterとformatterはどちらもプログラムの実行を伴わない静的解析ツールですが、それぞれ役割が違います。 この二つについて、当初勘違いをしていました。 コードがある基準に従って書かれているかをチェックするためのツールがlinter、基準に従うように自動修正するのがformatterだと思っていたのですが、少し違うようです。 linterとは、対象のコードがベストプラクティスに沿っているかどうかをチェックするツールです。例えばPythonで言うと、boolean値は==ではなくisで比較するとか、型の比較にはtype()ではなくisinstance()を使うとか、ラムダ式は変数に代入しない等、プログラムの動作上問題はないけれど良いコードにするために推奨されている書き方というのがあります。これらの書き方に合致しているかどうかをチェックするのがlinterです。またlinterによっては、deplicatedの関数の使用していないか、未使用・重複している変数がないかといった、実行時エラーにはならないが潜在的なエラーの原因になる箇所をチェックしてくれます。 基本的には問題箇所を警告するのがlinterの役割ですが、オプションで自動修正する機能を持つものもあります。ただし、これらの箇所の変更はプログラムの動作に影響を与えるため、自動修正は慎重に行う必要があります。 これに対しformatterは、コードのスタイルをチェックするツールです。コードのスタイルとは、スペースの数とか、改行の位置とか、コメントの書き方といった、プログラムの動作ではなく見た目に関わることです。formatterで自動修正を適用しても、通常はプログラムの動作には影響しません。 linterとは逆に、formatterは検出したエラーの自動修正が主目的ですが、エラーを警告するだけに留めることも可能です。 以上がlinterとformatterについての自分の理解です。…が、lintがチェック、formatが自動修正という分類も多くの場合は間違っていないので、この記事ではその意味でlinterとformatterという言葉を使い分けることにします。 linterとformatterでは役割が異なりますが、スタイルに関わる部分など、一部対象が被ることもあります。Pythonのlinterとformatterを完全に兼ね備えたツールは現状では存在しないため、それぞれ別々のツールを使う必要があります。 Pythonのコーディング規約 PythonにはPEP8という、公式コミュニティによって定められたコーディング規約が存在します。 PEP(Python Enhancement Proposal)はPythonの仕様についての提案をまとめた文書群です。その8番目の文書がPEP 8 – Style Guide for Python Codeで、Pythonの推奨される書き方についてまとめられています。見てみると分かりますが、lintとformatの両方のルールが定められています。 Pythonの多くのlinterやformatterは、基本的にこのPEP8に沿ってコードチェックを行います。 各ツールの比較 linter比較 Pythonのlinterにはいくつかありますが、有名なのはflake8とpylintの二つです。 pycodestyle pycodestyleは、PythonのコードがPEP8に準拠しているかをチェックするためのlinterです。以前はpep8という名前でしたが、文書の方のPEP8と名前が同じでややこしいので、pycodestyleという名前に改名されました。 コードスタイルについてのチェックも行うため、前述の厳密な分類によればformatterとしての側面も持つことになります。ただし、自動修正はできません。 シンプルなlinterですが、後述のflake8に内包されているため、現在では単体でこれを使うことはあまりないと思われます。 pyflakes こちらもlinterの一つですが、pycodestyleとは違い、コードスタイルについては一切チェックせず、未使用のimport文や変数などの、pycodestyleには検出できない論理的なエラーのみを検出します。 こちらも自動修正機能はなく、自動修正には後述のautoflakeを使います。 flake8 flake8は、以下の3つのlintツールをまとめたラッパーです。 pycodestyle pyflakes mccabe pycodestyleとpyflakesについては前述の通りです。 mccabeは循環的複雑度というコードの複雑度をチェックできるツールですが、flake8のデフォルトでは無効になっています。私の周りでも、mccabeが使われているところはあまり見たことがありません。ちなみに開発者のThomas McCabeさんの名前に由来しており、マッケイブと読みます。 このようにflake8は複数のコードチェックに対応した汎用的なlintツールで、この記事が書かれている時点で最も広く使われているlinterと言っていいと思います。 pylint pylintもPythonの汎用的なlinterの一つで、VSCodeのデフォルトのlinterはこれが使われています。オプションが豊富で多くのチェック項目に対応できるのが特徴です。flake8のライバル候補ですが、最近はflake8の方が支持されているようです。 formatter比較 flake8やpylintはコードスタイルのチェックも行ってくれるため、厳密に言えばformatterとしての側面も持ちますが、自動修正まではしてくれません。エラーを自動で修正したい場合は、別途コードフォーマットツールを使う必要があります。 formatterにもいくつか種類がありますが、現実的な選択肢として候補に上がるのは、autopep8とyapf、そしてblackの三つでしょうか。 autopep8 autopep8は、その名の通りPEP8に準拠するようにコードを整形するツールです。pycodestyleの検出したエラーを修正してくれますが、全てのエラーに対応しているわけではありません。(参考) Pythonのコードフォーマッターとしては古株で、特別悪い点もないのですが、最近はあまり使われていないようです。個人的には、過剰な改行を修正してくれない点が不満です。 ちなみに作者は日本人の方。 yapf googleが開発しているコードフォーマッターです。Yet Another Python Formatterの略だそうです。(yapfは何と発音するんでしょうか) カスタマイズ性が非常に高く、ベースのコードスタイルをpycodestyleやgoogle独自のスタイルから選ぶことができる他、多くの項目をオプションで設定できます。 天下のgoogleが開発していることもあり安心感は抜群で、機能も十分なのですが、最近はblackの人気に押されている印象です。 black 比較的最近登場した、新進気鋭のコードフォーマッター。 他のフォーマッターよりも制限が厳しく、設定できるオプションがかなり少ないことが特徴です。シングルクォートとダブルクォートの統一など、pycodestyleでは対応していないが不揃いだと気になりがちな、細かなスタイルまで統一してくれます。 カスタマイズ性が低い点がPythonerに好まれているのか、最近では非常に人気があります。Django・pandas・Poetryなど多くの有名プロジェクトでも採用されており、現在最も勢いのあるフォーマットツールです。 その他コード整形ツール 他のlinterやformatterが対応していないが、コードを綺麗に保つために有用なツールです。 autoflake autoflakeはpyflakesが検出したエラーを自動修正してくれるツールで、未使用のインポート文や変数を削除してくれます。 他のlinter・formatterにはこの機能はないため、このエラーを自動で修正したい場合はautoflakeを使う必要があります。 isort isortはimport文の並び順をチェックするツールです。linterの一種ですが、用途が限定的なのでこちらに書いています。並び順の自動修正も可能で、import文を標準ライブラリ→ローカルファイルの順、かつアルファベット順に並べ替えてくれます。 import文の順番についてはPEP8に規定がありますが、自動修正に対応しているツールが他にないため、import文の並び替えたいをしたい場合はisortが必要になります。 どのツールを使うか どのツールを使うかは、現在のトレンド・対応項目の網羅性・運用の楽さを考慮して決めます。 lint ソースコードのlint全般については、基本的にflake8を使い、flake8よりも細かいスタイルのチェックにはblackを使うのが良いです。さらにimport文の並び順のチェックにisortを使います。 import文の並び順はどうでもいいと言えばどうでもいいのですが、自分は結構気にするのでisortを使っています。import文が綺麗に並んでいるのは気持ちが良いです。 format ソースコードのformatについては、メインにblack、import文の並び替えにisortを使います。 autoflakeは使っても良いのですが、未使用のインポート文・変数を削除するためだけにツールが増えるのはやや渋いので、個人的にはなくてもいいと思います。自動修正しなくても、flake8が警告してくれるので、手動で削除すれば十分です。 各ツールの設定 black blackはほとんどオプションがないので、そもそも考える余地が少ないです。迷うとしたら、一行の文字数と、シングル・ダブルクォーテーションの統一くらいでしょうか。 一行の文字数はデフォルトだと88文字になっていますが、もう少し長くすることをお勧めします。周りのプロジェクトやOSSでは、119文字に設定しているところをよく見ます。119文字というのは、githubのコードレビュー画面の幅と同じだかららしいです。 シングルクォーテーションとダブルクォーテーションの使い分けについてはPEP8では特に規定されておらず、ただ「一貫せよ」とだけ書いてあります。blackではダブルクォーテーションに統一されます。二つはPythonでは全く同一ですが、慣習的に使い分けている人もいて反対意見も根強いためか、blackにしては珍しくオプションで無効化することができます。自分は統一する派です。なお、シングルクォーテーションに統一することはできません。 blackの設定は、コマンドラインのオプションで直接指定するか、pyproject.tomlで設定できます。自分はよくパッケージ管理にpoetryを使うので、poetryの設定と一緒にpyproject.tomlに書くことが多いです。 [tool.black] line-length = 119 コマンドラインのオプションで指定する場合は、black --line-length 119 target_file_or_directoryのようにします。blackはオプションが少ないので、他でpyproject.tomlを使っていなければコマンドラインで指定する方がシンプルだと思います。 クォーテーションの統一を無効化したい場合はpyproject.tomlにskip-string-normalization = trueの設定を追加します。コマンドラインの場合は--skip-string-normalizationを付けます。 flake8 flake8のデフォルトのルールには一部blackと競合するものがあるので、それらをblack側に合わせる必要があります。 競合を回避するための設定をblack公式が公開してくれているので(参考)、基本はその通りにします。flake8のデフォルトの一行の文字数は79文字なので、blackに設定した文字数で上書きします。 flake8の設定は.flake8、setup.cfg、tox.iniに書くことができます。自分はsetup.cfgに書いています。 [flake8] extend-ignore = E203 max-line-length = 119 isort isortのデフォルトのルールもblackといくつか競合します。 こちらもblack公式が対応箇所をまとめてくれているので、それをそのまま使います(参考)。isortのデフォルトの一行の文字数も79文字なので、blackに合わせます。 isortの設定は.isort.cfg、pyproject.toml、setup.cfg、tox.ini、.editorconfigに書くことができます。例えばpyproject.tomlに書く場合はこうなります。 [tool.isort] multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true line_length = 119 もしくはisortのバージョンが5以上の場合は、profile = blackとするだけで、isort公式が用意してくれている上記の設定を利用できます。設定がとてもシンプルになるのでおすすめです。 [tool.isort] profile = "black" line_length = 119 flake8とisortの設定ファイルは、他に使っているツールも考慮して、なるべく少ないファイルにまとめられるように選びます。自分はblackとisortの設定をpyproject.tomlに、flake8の設定をsetup.cfgに書いています。flake8がpyproject.tomlに対応してくれれば一つにまとめられるのですが。。 定期実行の方法 当然ですが、linterもformatterも設定しただけでは意味はなく、定期的に実行する必要があります。 定期実行する方法はいくつか考えられます。 直接コマンドを実行する 定期実行ではありませんが、ターミナルから直接lint・formatコマンドを実行することもよくあります。 lint・formatのためには複数のコマンドを実行する必要があるので、Makefileやシェルスクリプトを作ってワンライナーで実行できるようにしておくと便利です。 例えば自分は次のようなMakefileを作っています。 lint: flake8 target_file_or_directory isort --check --diff target_file_or_directory black --check target_file_or_directory format: isort target_file_or_directory black target_file_or_directory このようなMakefileを作っておけば、make lintでlintを、make formatでformatを実行できます。 ただし、windowsではデフォルトではmakeは使えません。 コードエディタと連携する 自分はVSCodeを使っているので他のエディタについては分かりませんが、VSCodeはPythonの自動lintingや保存時の自動修正に対応しています。 連携するためには.vscode/settings.jsonに以下の内容を設定します。 VSCodeではデフォルトのlinterとしてpylintが使われるので、それを無効化し代わりにflake8を有効にします。さらにformatterとしてblackとisortを使うように設定しています。autoflakeのフォーマットには対応していないようです。 { "python.linting.enabled": true, "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": true, "python.linting.lintOnSave": true, "python.formatting.provider": "black", "python.formatting.blackArgs": [ "--line-length", "119" ], "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true } } blackの設定を別ファイルに書いている場合はpython.formatting.blackArgsは不要です。 editor.formatOnSave: trueにすると保存時の自動スタイルフォーマットが、source.organizeImports: trueにすると保存時にimport文の自動ソートが有効になります。ただ、自分の予期しないタイミングでコードに変更が入るのが好きではないので、ファイル保存時の自動フォーマットは自分は使っていません。 Git Hooksを利用する gitにはhooksという、commitやpushなどの特定のアクションの前後にあらかじめ設定したスクリプトを自動で実行する仕組みがあります。この仕組みを使って、例えばコミットの直前にlintをかけるようにすれば、lintに失敗した場合はcommitが作成できなくなるため、フォーマットし忘れることがなくなります。 コミットの直前にスクリプトを実行したい場合は.git/hooks/pre-commitにそのスクリプトを書けばいいのですが、.git/ディレクトリ以下のファイルはgitの管理対象外なため、チーム間で共有することができません。そこで、pre-commitというPythonのツールを使います。 pre-commitは名前の通り、pre-commitのhooksの設定を簡単にしてくれるツールです。pip install pre-commitでインストールできます。このツールを使うと、.pre-commit-config.yamlという名前のファイルに設定を記述し、pre-commit installを実行することで、yamlファイルの内容に沿った.git/hooks/pre-commitを作成してくれます。 例えばコミットの直前にlintしたい場合、.pre-commit-config.yamlは次のようになります。 repos: - repo: local hooks: - id: lint name: lint entry: bash -c 'flake8 target_file_or_directory && isort -c --diff target_file_or_directory && black --check target_file_or_directory' language: system types: [python] Makefileを導入している場合は、代わりにmakeコマンドを使うこともできます。 repos: - repo: local hooks: - id: lint name: lint entry: bash -c 'make lint' language: system types: [python] この.pre-commit-config.yamlをコミットすることで、hooksの設定を共有することができるようになります。 コミット前のタイミングでlintだけでなくformatをかけることも可能ですが、予期しない変更がコミットに入り込む可能性があるので、lintに留めておくのが良いです。 CIツールを利用する ローカルではなくサーバーサイドでlintを実行する方法です。CircleCIやGitHub ActionsなどのCIツールを使ってコミットのpush時にlintをかけることで、formatのし忘れに気づくことができます。 GitHub Actionsのサンプルはこんな感じです。lintに失敗した場合はエラーになりその後のstepは実行されないため、formatし忘れたコードがビルドされたりmergeされてしまうことを防止できます。 name: Code Format Sample on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 isort black - name: Lint run: | flake8 target_file_or_directory isort --check --diff target_file_or_directory black --check --line-length 119 target_file_or_directory こちらもlintだけでなくformatをかけることも可能ですが、Git Hooksと同じ理由でお勧めはしません。 Git Hooksによるコミット前のlintとCIツールによるpush時のlintはタイミングが被るため、両方でlintするのはやや過剰で、どちらか一方を使えば十分だと思います。 実際の運用方法 まず、前述した通りに、Makefileの作成・VSCodeのlinter連携・Github Actionsの設定をしておきます。 開発をする際は、コミット直前にmake formatでフォーマットを実行します。その上でVSCodeのlintエラーが出ていないか確認し、エラーが出ていれば手動で修正します。もしフォーマッターを適用し忘れたとしても、Github Actionsの方でエラーになるので、マージされる前に気付くことができます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む