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

【Python】scheduleを使ってモジュールを定期実行させよう

モジュールを定期的に実行させるのに便利なscheduleモジュールを紹介します。scheduleは一定の間隔(数分・数時間・数日おき)に同じ作業を実行するような場合に便利なモジュールです。webスクレイピングで情報収集を行う場面などで便利なモジュールとなります。

今回は基本的なscheduleモジュールの使用法をまとめた後、スクレイピングを定期的に実行するモジュールを実装例としてご紹介します。

scheduleのインストール

scheduleモジュールはpipコマンドで入手可能です。コマンドプロンプト上で以下のコマンドを実行するとインストールされます。

pip install schedule

サンプルコード

sample_schedule.py
import schedule
import time

# 実行job関数
def job():
    print("job実行")


#1分毎のjob実行を登録
schedule.every(1).minutes.do(job)

#1時間毎のjob実行を登録
schedule.every(1).hours.do(job)

#AM11:00のjob実行を登録
schedule.every().day.at("11:00").do(job)

#日曜日のjob実行を登録
schedule.every().sunday.do(job)

#水曜日13:15のjob実行を登録
schedule.every().wednesday.at("13:15").do(job)

# jobの実行監視、指定時間になったらjob関数を実行
while True:
    schedule.run_pending()
    time.sleep(1)

scheduleのサンプルモジュールです。ある間隔ごとにjob関数が実行され、「job実行」が表示されるモジュールとなります。schedule.everyは実行するjobと実行間隔を登録する記述となります。数分ごとや数時間ごと、さらに特定日時の実行も可能です。

# 1分毎にjobを実行
schedule.every(1).minutes.do(job)

上記のschedule.everyで登録したjobは以下のschedule.run_pending()で実行されます。普通にschedule.run_pending()を呼び出しただけでは、一度jobを実行した時点で、モジュールが終了してしまうため、while文で無限ループ状態にする必要があります。無限ルール状態にすることで、同じ処理を一定間隔で実し続けることが可能です。

while True:
    schedule.run_pending()
    time.sleep(1)

scheduleを使ってYahooニュースの採取を定期実行

今回紹介したscheduleモジュールを使用して、定期的にスクレイピングを実行するようなモジュールを作成しました。1時間ごとにYahooニュースにアクセスし、ニュースのタイトルとURLを取得するようなモジュールです。

scraping_schedule.py
from urllib.request import urlopen
from urllib.error import HTTPError
from urllib.error import URLError
from bs4 import BeautifulSoup
import re
import schedule
import time

def job():
    try:
        html = urlopen('https://news.yahoo.co.jp/topics')
    except HTTPError as e:
        print(e)
    except URLError as e:
        print(e)

    else:

# Yahooトピックスにアクセスし、ニュース情報を採取
        bs = BeautifulSoup(html.read(), 'lxml')
        newsList = bs.find('div', {'class': 'topicsListAllMain'}).find_all('a')

# 取得したListからニュースのタイトルとURLを取得して表示
        for news in newsList:
            if re.match('^(https://)', news.attrs['href']):
                print(news.get_text())
                print(news.attrs['href'])

#1時間毎にjobを実行
schedule.every(1).hours.do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

まとめ

今回は定例的ないjobを一定間隔で実行させることができるscheduleモジュールをご紹介しました。私は主にスクレイピングなどで活用することが多いですが、作業の定期実行にも使えるモジュールではあるので、使用用途はとても広いと思います。

参考文献

Python Scheduleライブラリでジョブ実行
scheduleライブラリを使ってPythonスクリプトを定期実行しよう

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

TensorflowのFreezeGraph済み.pbファイルのINPUTのPlaceholderを置き換えつつモデルを再生成するTips [置換・置き換え・変換・変更・更新・差し替え]

[1, ?, ?, 3] の入力サイズで定義されている .pbファイル の Placeholder[1, 513, 513, 3]Placeholder に強制的に置き換えて、.pbを再生成するサンプルプログラム。

name='image' の部分は置き換え後のPlaceholderの名前を自由に指定する。
input_map={'image:0': inputs}image:0 の部分は、変換前のモデルのPlaceholder名を指定する。

replacement_of_input_placeholder.py
import tensorflow as tf
from tensorflow.tools.graph_transforms import TransformGraph

with tf.compat.v1.Session() as sess:

    # shape=[1, ?, ?, 3] -> shape=[1, 513, 513, 3]
    # name='image' specifies the placeholder name of the converted model
    inputs = tf.compat.v1.placeholder(tf.float32, shape=[1, 513, 513, 3], name='image')
    with tf.io.gfile.GFile('./model-mobilenet_v1_101.pb', 'rb') as f:
        graph_def = tf.compat.v1.GraphDef()
    graph_def.ParseFromString(f.read())

    # 'image:0' specifies the placeholder name of the model before conversion
    tf.graph_util.import_graph_def(graph_def, input_map={'image:0': inputs}, name='')
    print([n for n in tf.compat.v1.get_default_graph().as_graph_def().node if n.name == 'image'])

    # Delete Placeholder "image" before conversion
    # see: https://github.com/tensorflow/tensorflow/tree/master/tensorflow/tools/graph_transforms
    # TransformGraph(
    #     graph_def(),
    #     input_op_name,
    #     output_op_names,
    #     conversion options
    # )
    optimized_graph_def = TransformGraph(
                              tf.compat.v1.get_default_graph().as_graph_def(),
                              'image',
                              ['heatmap','offset_2','displacement_fwd_2','displacement_bwd_2'],
                              ['strip_unused_nodes(type=float, shape="1,513,513,3")'])

    tf.io.write_graph(optimized_graph_def, './', 'model-mobilenet_v1_101_513.pb', as_text=False)
Result
[name: "image"
op: "Placeholder"
attr {
  key: "dtype"
  value {
    type: DT_FLOAT
  }
}
attr {
  key: "shape"
  value {
    shape {
      dim {
        size: 1
      }
      dim {
        size: 513
      }
      dim {
        size: 513
      }
      dim {
        size: 3
      }
    }
  }
}
]
  • 変換前 Screenshot 2019-12-30 23:39:02.png
  • 変換後 Screenshot 2019-12-30 23:38:04.png

Graph Transform Tool
https://github.com/tensorflow/tensorflow/tree/master/tensorflow/tools/graph_transforms

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

pandas.DataFrameで、特定の列だけ代入するときでも、indexがついていれば、データの順番は気にしなくていい

発見したこと

DataFrameに列単位で、DataFrame型の値を代入するとき、データ順がばらばらでも、indexが一致していれば、そのまま代入できます。
DataFrameから一部の列を抜き出して、条件により加工し、戻す、というときに、知っておくと便利かもです。

試した環境

  • python 3.7.4
  • pandas 0.25.3

コード例

モジュールとデータの準備をして

import numpy as np
import pandas as pd

# 0-19の数列を4*5に整形し、dataframe化
df = pd.DataFrame(np.arange(20).reshape((4,5)), columns = list("abcde"))

#     a   b   c   d   e
# 0   0   1   2   3   4
# 1   5   6   7   8   9
# 2  10  11  12  13  14
# 3  15  16  17  18  19

一部の列を抜き出したDataFrameを作って、値を加工します

df_e_1 = df.loc[[0,2], ["e"]]
#     e
# 0   4
# 2  14

df_e_1["e"] += 300
#      e
# 0  304
# 2  314

一部の列を抜き出したDataFrameを作って、上で加工したデータと結合します。

df_e_2 = df.loc[[1,3], ["e"]]
#     e
# 1   9
# 3  19

df_e = pd.concat([df_e_1, df_e_2])
#      e
# 0  304
# 2  314
# 1    9
# 3   19

ここでdf_eをindex順に整列しないともとのDataFrameに代入してもデータ順がばらばらになってしまう、と思っていたけれど・・
そのまま代入しても、indexを一致させて自動で並べ替えて代入してくれます。
以下のe欄に注目ください

df["e"] = df_e
print(df)
#     a   b   c   d    e
# 0   0   1   2   3  304
# 1   5   6   7   8    9
# 2  10  11  12  13  314
# 3  15  16  17  18   19
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

出題テーマ

  • 真理値表
  • マクロ

問題文

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

(1)

def solve1(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
        ret = text.split('+')
        return ret    
def print1(ret):
    for txt in ret:
        print(txt)

(2)

def solve1(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
        ret = text.split('+')
        return ret 

def solve2(file_path):
    txts = solve1(file_path)
    al_set = set()
    groups = []
    for index, txt in enumerate(txts):
        als = txt.split('&')
        group = []
        for al in als:
            group.append(al)
            al_set.add(al)
        group.sort()
        groups.append(group) 

    al_set2 = []   
    for al in al_set:
        al_set2.append(al)
    al_set2.sort()    

    answers = set()
    for group in groups:
        ans = ['']
        for al in al_set2:
            if al in group:
                for i in range(len(ans)):
                    ans[i] += '{0}=true '.format(al)
#                     print(ans)
            else:
                tmp = len(ans)
                ans = ans * 2
                for i in range(tmp):
                    ans[i] += '{0}=true '.format(al)
                    ans[i+tmp] += '{0}=false '.format(al)
        for txt in ans:
            answers.add(txt)
    if len(answers) > 0:
        for a in answers:
            print(a)
    else:
        print('none')

(3)

def solve1(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
        ret = text.split('+')
        return ret

# a&!aが存在するかどうか  
# groupでは!aをa!の形で持っている
def isNone(group):
    for al in group:
        if (al+'!') in group:
            return True
    return False    

def solve3(file_path):
    txts = solve1(file_path)
    al_set = set()
    groups = []
    for index, txt in enumerate(txts):
        als = txt.split('&')
        group = []
        for al in als:
            last = al[-1]
            if (len(al) % 2) == 0:
                group.append(last+'!')
            else:
                group.append(last)
            al_set.add(last)
        group.sort()
        groups.append(group)    

    al_set2 = []   
    for al in al_set:
        al_set2.append(al)
    al_set2.sort()    

#     print(al_set2)
#     print(groups)
#     return

    answers = set()
    for group in groups:
        if not isNone(group):
            ans = ['']
            for al in al_set2:
                if al in group:
                    for i in range(len(ans)):
                        ans[i] += '{0}=true '.format(al)
                elif (al+'!') in group:
                    for i in range(len(ans)):
                        ans[i] += '{0}=false '.format(al)
                else:
                    tmp = len(ans)
                    ans = ans * 2
                    for i in range(tmp):
                        ans[i] += '{0}=true '.format(al)
                        ans[i+tmp] += '{0}=false '.format(al)
            for txt in ans:
                answers.add(txt)
    if len(answers) > 0:
        for a in answers:
            print(a)
    else:
        print('none')

(4)

from N_DIGIT import baseNumbers

alpha = ['a', 'b', 'c', 'd', 'e', 
         'f', 'g', 'h', 'i', 'j', 
         'k', 'l', 'm', 'n', 'o',
         'p', 'q', 'r', 's', 't',
         'u', 'v', 'w', 'x', 'y', 'x',
        ]

def get_alpha_set(text):
    ret = []
    for al in alpha:
        if al in text:
            ret.append(al)
    ret.sort()    
    return ret

def macro(text):
        text = text.replace('!', ' not ')
        text = text.replace('&', ' and ')
        text = text.replace('+', ' or ')
        return text       

def solve4(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
    al_set = get_alpha_set(text)
    al_dic = {}
    for index, al in enumerate(al_set):
        al_dic[al] = index
    bs = baseNumbers(1 << len(al_set), 2, len(al_set))
    ans = []
    for b in bs:
        tmp = text
        for al in al_set:
            tmp = tmp.replace(al, str(b[al_dic[al]]))
        formatted_tmp = macro(tmp)
        if eval(formatted_tmp):
            ans.append(b)
    for a in ans:
        txt = ''
        for index, boolean in enumerate(a):
            if boolean:
                al = al_set[index]
                txt += '{0}=true '.format(al)
            else:
                al = al_set[index]
                txt += '{0}=false '.format(al)
        print(txt)    

(5)

from N_DIGIT import baseNumbers

alpha = ['a', 'b', 'c', 'd', 'e', 
         'f', 'g', 'h', 'i', 'j', 
         'k', 'l', 'm', 'n', 'o',
         'p', 'q', 'r', 's', 't',
         'u', 'v', 'w', 'x', 'y', 'x',
        ]

def get_alpha_set(text):
    ret = []
    for al in alpha:
        if al in text:
            ret.append(al)
    ret.sort()    
    return ret

def macro(text):
        text = text.replace('!', ' not ')
        text = text.replace('&', ' and ')
        text = text.replace('+', ' or ')
        return text       

def solve5(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
    al_set = get_alpha_set(text)
    al_dic = {}
    for index, al in enumerate(al_set):
        al_dic[al] = index
    bs = baseNumbers(1 << len(al_set), 2, len(al_set))
    ans = []
    for b in bs:
        tmp = text
        for al in al_set:
            tmp = tmp.replace(al, str(b[al_dic[al]]))
        formatted_tmp = macro(tmp)
        if eval(formatted_tmp):
            ans.append(b)

    txt = ''
    for a in ans:
        tmp = ''
        for index, boolean in enumerate(a):
            if boolean:
                al = al_set[index]
                tmp += (al+'&')
            else:
                al = al_set[index]
                tmp += ('!'+al+'&')
        txt += (tmp[:-1]+'+')
    print(txt[:-1])

(6)

from N_DIGIT import baseNumbers

alpha = ['a', 'b', 'c', 'd', 'e', 
         'f', 'g', 'h', 'i', 'j', 
         'k', 'l', 'm', 'n', 'o',
         'p', 'q', 'r', 's', 't',
         'u', 'v', 'w', 'x', 'y', 'x',
        ]

def get_alpha_set(text):
    ret = []
    for al in alpha:
        if al in text:
            ret.append(al)
    ret.sort()    
    return ret

def macro(text):
        text = text.replace('!', ' not ')
        text = text.replace('&', ' and ')
        text = text.replace('+', ' or ')
        return text       

def solve6(file_path):
    with open(file_path, 'r') as f:
        text = f.read()
        if text[-1] == '\n':
            text = text[:-1]
    al_set = get_alpha_set(text)
    al_dic = {}
    for index, al in enumerate(al_set):
        al_dic[al] = index
    bs = baseNumbers(1 << len(al_set), 2, len(al_set))
    ans = []
    for b in bs:
        tmp = text
        for al in al_set:
            tmp = tmp.replace(al, str(b[al_dic[al]]))
        formatted_tmp = macro(tmp)
        if not eval(formatted_tmp):
            ans.append(b)

    txt = ''
    for a in ans:
        tmp = '('
        for index, boolean in enumerate(a):
            if boolean:
                al = al_set[index]
                tmp += ('!'+al+'+')
            else:
                al = al_set[index]
                tmp += (al+'+')
        txt += (tmp[:-1]+')&')
    print(txt[:-1])

感想

  • コンパイラと見せかけた真理値表の問題
  • (3)までは解析を実装して、そこから解を求めていく形にしたが(4)から()が登場し、これの何がまずいかというと()内()がこの場合ありえてしまうのでsplitなどによって単純に分割できなくなった。実際に()つきの解析プログラムを実装することは可能(当たり前)だがそれは言語処理論の分野であり、導出木の作成が必要で非常に厄介と感じたため、全探索とマクロを使う形にした(最初からこのやり方の方が実は楽)。
  • ただ全探索とマクロを組み合わせたやり方だと最後までこのやり方でできてしまうので、出題者の意図に反しているのではないかと迷ったので筆者は(3)まではきちんと解析を実装した形にした。
  • 全探索の場合は最大で2^26(アルファベット分)の約10^7~10^8の計算量が必要であるがこれはまぁ現実的な範囲では一応ある。
  • 大抵のプログラミング言語はnot > and > orの優先順位であるのでマクロの利用を想定しているとも取れる。
  • 加法の方はまぁ(4)そのままでいいだろう、乗法の方はこれも論理回路の教科書などには必ず載っている内容であるので知っていると加法同様に(4)を利用してすぐに実装ができる。知らないとド・モルガンの法則から自力で気付くのは結構きついと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JupyterNotebookでの「No module named '〇〇'」エラーの対処法|!pipでインストールしよう!

JupyterNotebook初心者向けの記事です。
※記事内のミスがありましたら、コメントにてお教えください。

事象:No module named 'pandas'のようなエラーが発生

import pandas as pd

のようなコードを実行し、

ModuleNotFoundError     Traceback (most recent call last)
<ipython-input-23-7dd3504c366f> in <module>
----> 1 import pandas as pd

ModuleNotFoundError: No module named 'pandas'

のエラーが発生したとき。

原因:モジュールがjupyternotebook上にインストールされていない

実行しようとしていたモジュール(numpyやpandasなど)が、jupyternotebook上でインストールされていない可能性があります。
※terminal上や、その他IDEでインストールしていても、jupyternotebook上でモジュールを動かしたいときは、別途インストールが必要です。

対処法:!pip installでインストールし直し

!pip install pandas

!pipというコマンドを使って、モジュールをインストールし直しましょう。「!」を先頭につけることで、jupyternotebook上でも、システムコマンドを実行できます。

結果:Successfully installed 〇〇と出ていれば成功

Collecting numpy
Downloading https://files.pythonhosted.org/packages/7c/cd/5243645399c09bb5081e8d2847583f7a6b7cca55eb096a880eda0b602d4d/numpy-1.18.0-cp36-cp36m-macosx_10_9_x86_64.whl (15.2MB)
     |████████████████████████████████| 15.2MB 48kB/s eta 0:00:016
Installing collected packages: numpy
Successfully installed numpy-1.18.0 

このようにインストールが完了すれば成功です。

スクリーンショット 2019-12-30 19.51.50.png

再インストール後は、↑のように再度使いたいモジュールをimportを使って呼び出してみてください。

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

TensorFlow2 + Keras による画像分類に挑戦3 ~MNISTデータを可視化してみる~

はじめに

Google Colaboratory 環境で TensorFlow2 + Keras を利用した画像分類の勉強メモです。ド定番である手書き数字画像(MNIST)の分類を題材にします。

前回は、MNISTデータを取得し、そのデータの構造や内容について確認しました。手書きの数字の画像データに相当する入力データは、28x28pixelの256段階グレースケールでした。このデータの型は numpy.ndarray の2次元配列で、そのまま print するだけでも、なんとか内容(画像イメージ)をつかむことができましたが、今回は matplotlib を使って、次のようにきれいに表示させてみたいと思います。

xtrain3.png

データの正規化

MNISTのデータは、0~255の整数値を使って、256段階グレースケール(白を0、黒を255に割り当てたグレースケール)を表現していました(詳しくは前回参照)。しかし、TensorFlow を使った画像分類のサンプルコード(公式HPのチュートリアル参照)では、機械学習させる都合上、次のように 0.0~1.0 の範囲になるように正規化を施しています。

import tensorflow as tf
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0 # 正規化処理

ここから先は、0.0~1.0 に正規化されたデータを対象に進めていきたいと思います。

とりあえず表示

トレーニング用の入力データの1個目 x_train[0] をグレースケール画像として出力してみたいと思います。このデータは、正解データ y_train[0] に格納されているように「5」を表現した画像になります。

import matplotlib.pyplot as plt
plt.figure(dpi=96)
plt.imshow(x_train[0],interpolation='nearest',vmin=0.,vmax=1.,cmap='Greys')

xtrain0_1.png

実行環境によっては、interpolation='nearest'は省略できます(Google Colab.では省略してもOKです、他環境で実行してぼやけた出力になったら、このオプションを明示しましょう)。

また、vmin=0.,vmax=1. は、当該データ x_train[?] の内部要素の最小値が 0.0、最大値が 1.0 の場合は省略してもOKです(cmap='Greys' により0.0に白、1.0に黒が割り当てられます)。そうでない場合、例えば、薄文字などを表現していてx_train[?]の内部要素の最大値が0.7のようなときは、このオプションを指定しないと、薄文字の感じが反映されません。

キーワード引数 cmap の値を変えると、出力に使用するカラーマップを変えることができます。プリセットとして用意されているカラーマップ一覧は、matplotlibのリファレンスで確認することができます。例えば、cmap='Greens'とすると次のような出力になります(0.0のところも薄緑になります)。

xtrain0_1g.png

カラーマップをカスタマイズすることも可能です。具体的な方法は「相関行列をキレイにカスタマイズしたヒートマップで出力したい。matplotlib編 @ Qiita」を参照ください。

特定の数字についての手書き画像を並べて出力

特定の数字(例えば「7」)について、どんなで手書きデータが存在するのか確認したいときには、次のようなコードで出力することができます。

import numpy as np
import matplotlib.pyplot as plt
x_subset = x_train[ y_train == 7 ]   # (1)
fig, ax = plt.subplots(nrows=8, ncols=8, figsize=(5, 5), dpi=120)
for i, ax in enumerate( np.ravel(ax) ):
  ax.imshow(x_subset[i],interpolation='nearest',vmin=0.,vmax=1.,cmap='Greys')
  ax.tick_params(axis='both', which='both', left=False, 
                 labelleft=False, bottom=False, labelbottom=False) # (2)

実行結果は次のようになります。7以外の数値について出力したい場合は、上記コードの (1) の y_train == 7 の数値を変更してください。(2) の ax.tick_params(...) は、X軸・Y軸の目盛を消すためのものです。

xtrain1.png

一覧で眺めてみると、この64枚のなかであっても、どうみても「1」にしか見えないものが少なくとも2、3個は含まれているということが分かります(つまり、正答率 1.0000 は極めて難しい)。

整形して表示

非常に短いコードで入力データを画像化して出力できることが分かりました。

ここでは、次のように、各入力データの何行何列目の要素がどんな値になっているのか?までを確認できるように手を加えていきます。左上の赤文字は、対応する正解データの値です。

xtrain3.png

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as pe
import matplotlib.transforms as ts

i = 2 # 表示するデータのインデックス

plt.figure(dpi=120)
plt.imshow(x_train[i],interpolation='nearest',vmin=0.,vmax=1.,cmap='Greys')

h, w = 28, 28
plt.xlim(-0.5,w-0.5) # X軸方向の描画範囲
plt.ylim(h-0.5,-0.5) # Y軸方向の・・・

#
plt.tick_params(axis='both', which='major', 
                left=False, labelleft=False, 
                bottom=False, labelbottom=False)
plt.tick_params(axis='both', which='minor',
                left=False, labelleft=True,
                top=False, labeltop=True, 
                bottom=False, labelbottom=False)

# 各軸のグリッド設定
plt.gca().set_xticks(np.arange(0.5, w-0.5,1)) # 1ドット単位でグリッド
plt.gca().set_yticks(np.arange(0.5, h-0.5,1))
plt.grid( color='tab:green', linewidth=1, alpha=0.5)

# 各軸のラベル設定
plt.gca().set_xticks(np.arange(0, w),minor=True)
plt.gca().set_xticklabels(np.arange(0, w),minor=True, fontsize=5)
plt.gca().set_yticks(np.arange(0, h),minor=True)
plt.gca().set_yticklabels(np.arange(0, h),minor=True, fontsize=5)

# ラベルの位置の微調整
offset = ts.ScaledTranslation(0, -0.07, plt.gcf().dpi_scale_trans)
for label in plt.gca().xaxis.get_minorticklabels() :
    label.set_transform(label.get_transform() + offset)
offset = ts.ScaledTranslation(0.03, 0, plt.gcf().dpi_scale_trans)
for label in plt.gca().yaxis.get_minorticklabels() :
    label.set_transform(label.get_transform() + offset)

# 正解データを左上に表示(白色で縁取り)
t = plt.text(1, 1, f'{y_train[i]}', verticalalignment='top', fontsize=20, color='tab:red')
t.set_path_effects([pe.Stroke(linewidth=5, foreground='white'), pe.Normal()])

plt.colorbar( pad=0.01 ) # 右側にカラーバー表示

グレースケール値のヒストグラム

入力データは、$28\times 28 = 784$ 個の要素から構成され、各要素には 0.0 から 1.0 の値が含まれますが、それはどんな分布になっているかヒストグラムを作成してみたいと思います。

import numpy as np
import matplotlib.pyplot as plt

i = 0 # 表示するデータのインデックス

h = plt.hist(np.ravel(x_train[i]), bins=10, color='black')
plt.xticks(np.linspace(0,1,11))
print(h[0]) # 実行結果 -> [639.  11.   6.  11.   6.   9.  11.  12.  11.  68.]
print(h[1]) # 実行結果 -> [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]

hist.png

plt.hist(...) の戻値には、各階級の度数が含まれます。上記の例だと、範囲 $0.0\le v < 0.1 $ の値を持つピクセルが 639個存在することが分かります。なお、一番右端のみ、範囲は $0.9\le v \le 1.0 $ となり、値がちょうど 1.0 のデータも含んだものになります。

実際に print(h[0].sum()) にすれば、784.0 が得られ、値がちょうど 1.0 の要素もちゃんとカウントされていることが確認できます。

次回

  • 学習済みのモデルを使って実際に予測を行ないます。自作の手書きでデータを入力とした予測も行ないます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

出題テーマ

  • 文字列探索

問題文

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

(1)

def solve1(file_path):
    cnt = 0
    with open(file_path, 'r') as f:
        txt = f.read()
        if txt[-1] == '\n' and len(txt) > 1:
                txt = txt[:-1]
        for ch in txt:
            if ch == ';':
                cnt += 1
    return cnt

(2)

def solve2(file_path):
    with open(file_path, 'r') as f:
        txts = f.readlines()
    ret = []
    for index, txt in enumerate(txts):
        if 'main' in txt:
            print('{0}行目: {1}'.format(index+1, txt))

(3)

def solve3(file_path):
    with open(file_path, 'r') as f:
        text = f.readlines()
        txts = []
        for index, txt in enumerate(text):
            if txt[-1] == '\n':
                txts.append(txt[:-1])
            else:
                txts.append(txt)
        s = set()
        for index, txt in enumerate(txts):
            if index < len(txts) - 1:
                next_txt = txts[index + 1]
                if txt == next_txt:
                    s.add(txt)                
    for txt in s:
        print(txt)

(4)

class Line(object):
    def __init__(self, txt, initial_appear, appear_times):
        self.txt = txt
        self.initial_appear = initial_appear
        self.appear_times = appear_times

    def __lt__(self, line):
        self.initial_appear < line.initial_appear

    def __repr__(self):
        return '初登場行: {0}, 登場回数: {1}, txt: {2}'.format(self.initial_appear, self.appear_times, self.txt)

def solve4(file_path):
    with open(file_path, 'r') as f:        
        text = f.readlines()
        txts = []
        for index, txt in enumerate(text):
            if txt[-1] == '\n':
                txts.append(txt[:-1])
            else:
                txts.append(txt)

        dic = {} # 'txt': [初登場行, 登場回数]
        for index, txt in enumerate(txts):
            if txt in dic.keys():
                dic[txt][1] += 1
            else:
                dic[txt] = [index+1, 1]
    ret = []
    for key in dic.keys():
        obj = dic[key]
        initial_appear = obj[0]
        appear_times = obj[1]
        if appear_times > 1:
            ret.append(Line(key, initial_appear, appear_times))

    sorted(ret)
    for line in ret:
        print(line)

(5)

def formatnptxt(nparray):
    txt = ''
    for ch in nparray:
        txt += ch
    return txt    

def solve5(file_path, minimum_len):
     with open(file_path, 'r') as f:        
        text = f.readlines()
        txts = []
        max_len = 0
        for index, txt in enumerate(text):
            if txt[-1] == '\n':
                if len(txt[:-1]) >= minimum_len:
                    txts.append(txt[:-1])
                    max_len = max(max_len, len(txt[:-1]))
            else:
                if len(txt) >= minimum_len:
                    txts.append(txt)
                    max_len = max(max_len, len(txt))                    

        formatted_txts = np.array([[' ' for _ in range(max_len)] for _ in range(len(txts))])
        for i, txt in enumerate(txts):
            for j, ch in enumerate(txt):
                formatted_txts[i, j] = ch
        ret = set()
        sames = 0
        for i in range(0, len(formatted_txts) - 1):
            txt1 = formatted_txts[i]
            for j in range(i+1, len(formatted_txts)):
                txt2 = formatted_txts[j]
                booleans = (txt1 == txt2)
                same_scores = booleans.sum()
                if same_scores < max_len and (max_len - same_scores) < 5:
                    sames += 1
                    pair1 = '{0}\n{1}'.format(formatnptxt(txt1), formatnptxt(txt2))
                    pair2 = '{0}\n{1}'.format(formatnptxt(txt2), formatnptxt(txt1))                    
                    if (not (pair1 in ret)) and (not (pair2 in ret)):
                        ret.add(pair1)

        for set_ele in ret:
            set_ele_array = set_ele.split('\n')
            print('{0}, {1}'.format(set_ele_array[0], set_ele_array[1]))
        print(sames)

(6)

from LCS import LCS

def solve6(file_path, minimu_len):
    with open(file_path, 'r') as f:
        text = f.readlines()
        txts = []
        for index, txt in enumerate(text):
            if txt[-1] == '\n':
                if len(txt[:-1]) >= minimu_len:
                    txts.append(txt[:-1])
            else:
                if len(txt) >= minimu_len:
                    txts.append(txt)

        s = set()
        sames = 0
        for i in range(len(txts) - 1):
            txt1 = txts[i]
            txt1_len = len(txt1)
            for j in range(i + 1, len(txts)):
                txt2 = txts[j]
                txt2_len = len(txt2)
                if txt1 != txt2:
                    lcs = LCS(txt1, txt2)
                    lcs_len = len(lcs)
                    diff = abs(txt1_len - txt2_len) + min(txt1_len - lcs_len, txt2_len - lcs_len)
                    if diff < 4:
                        sames += 1
                        pair1 = '{0}\n{1}'.format(txt1, txt2)
                        pair2 = '{0}\n{1}'.format(txt2, txt1)
                        if (not (pair1 in s)) and (not (pair2 in s)):
                            s.add(pair1)
        for set_ele in s:
            set_ele_array = set_ele.split('\n')
            print('{0}, {1}'.format(set_ele_array[0], set_ele_array[1]))
        print(sames)

(7) 時間内に解き終わりませんでした...

やり方をちょっと考えたのですが, n行とすると
4,5,6...n/2ブロックサイズで順に探索して, 例えば4のとき、[1,4]と[12,15]が一致して5のとき
[1,5]と[12,16]が一致したら、前者を削除するみたいな感じでやればできそうな気がします。

感想

  • (6)ではtext1からtext2またはその逆の変換コストは等しいので、ここでは短い方を長い方に作り変える視点で考えて、まずLCS(最長共通部分列)をみて、短い方との差(min(txt1_len - lcs_len, txt2_len - lcs_len))をまず長い方に合わせて作り変えると、短い方の長さの文字列ができ、あとは長い方との文字数差分abs(txt1_len - txt2_len)を追加することでできるのでコード上のような内容が最小ステップ数となります。(たぶん笑)
  • (7)は純粋に時間足りなかったです...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【python】outlookでメール送信

はじめに

仕事でexcelでメール作成マクロ作ったけど
「excelだと起動わずらわしいし、excelありきになるからなんかやだな…」
と思い、業務外ってことでリラックスした気持ちで探したら
神がかり的にわかりやすいサイトが見つかったので作ってみた。

参考サイト

https://towel-memo.com/python/email_python/

#ライブラリ
import win32com.client

#Outlookのオブジェクト設定
outlook = win32com.client.Dispatch('Outlook.Application')
mymail = outlook.CreateItem(0)

#署名
sign = '''
株式会社 ホゲホゲドットコム
世界のナベヒロ
'''


#メールの設定
mymail.BodyFormat = 1
mymail.To = 'foo@hoge.co.jp; bar@hoge.co.jp'
mymail.cc = 'foo@hoge.com'
mymail.Bcc = 'bar@hoge.com'
mymail.Subject = '件名'
mymail.Body = '''各位
お疲れ様です。

以上、よろしくお願いいたします。
'''+ '\n' +sign

path = r'C:\\Users\watya\Desktop\hogehoge.txt' # 添付ファイルは絶対パスで指定
mymail.Attachments.Add (path)

#出来上がったメール確認
mymail.Display(True)
#確認せず送信する場合は、mymail.Display(True)を消して、下記コードを使用する
#mymail.Send()

実行結果

 mymail.Display(True)
で実行したので、メールは作成されたけど
送信まではされず下書き状態でストップしてくれた。やさしい。。。
これはテンプレ通りのメールを作成するのに便利ですね。

しかもfromはログイン時のOutlookアドレスを使用してくれるので
プログラム作成時に意識する必要もないのでとっても楽。
これは便利!

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

今日のpython error:

プログラムちょい替え(2)Python3: URLをコマンドライン引数で(wikipedia)
https://qiita.com/kaizen_nagoya/items/fc095b0c580a35001ea7

の作業をdockerで実施中。

docker(89) dockerでpython2, python3
https://qiita.com/kaizen_nagoya/items/ecbe11a4d743357134d5

docker/ubuntu
# pip install bs4
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support
Collecting bs4
  Downloading https://files.pythonhosted.org/packages/10/ed/7e8b97591f6f456174139ec089c769f89a94a1a4025fe967691de971f314/bs4-0.0.1.tar.gz
Collecting beautifulsoup4
  Downloading https://files.pythonhosted.org/packages/c5/48/c88b0b390ae1f785942fc83413feb1268a1eb696f343d4d55db735b9bb39/beautifulsoup4-4.8.2-py2-none-any.whl (106kB)
     |################################| 112kB 1.7MB/s 
Collecting soupsieve>=1.2
  Downloading https://files.pythonhosted.org/packages/81/94/03c0f04471fc245d08d0a99f7946ac228ca98da4fa75796c507f61e688c2/soupsieve-1.9.5-py2.py3-none-any.whl
Collecting backports.functools-lru-cache; python_version < "3"
  Downloading https://files.pythonhosted.org/packages/da/d1/080d2bb13773803648281a49e3918f65b31b7beebf009887a529357fd44a/backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl
Building wheels for collected packages: bs4
  Building wheel for bs4 (setup.py) ... done
  Created wheel for bs4: filename=bs4-0.0.1-cp27-none-any.whl size=1273 sha256=79b8b3765197c5d2662611ba7f1199b00147c4741c41f4dc9cdc82d0f08f0609
  Stored in directory: /root/.cache/pip/wheels/a0/b0/b2/4f80b9456b87abedbc0bf2d52235414c3467d8889be38dd472
Successfully built bs4
Installing collected packages: backports.functools-lru-cache, soupsieve, beautifulsoup4, bs4
Successfully installed backports.functools-lru-cache-1.6.1 beautifulsoup4-4.8.2 bs4-0.0.1 soupsieve-1.9.5

# pip3 install bs4
Collecting bs4
  Cache entry deserialization failed, entry ignored
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/10/ed/7e8b97591f6f456174139ec089c769f89a94a1a4025fe967691de971f314/bs4-0.0.1.tar.gz
Collecting beautifulsoup4 (from bs4)
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/cb/a1/c698cf319e9cfed6b17376281bd0efc6bfc8465698f54170ef60a485ab5d/beautifulsoup4-4.8.2-py3-none-any.whl (106kB)
    100% |################################| 112kB 2.4MB/s 
Collecting soupsieve>=1.2 (from beautifulsoup4->bs4)
  Cache entry deserialization failed, entry ignored
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/81/94/03c0f04471fc245d08d0a99f7946ac228ca98da4fa75796c507f61e688c2/soupsieve-1.9.5-py2.py3-none-any.whl
Building wheels for collected packages: bs4
  Running setup.py bdist_wheel for bs4 ... done
  Stored in directory: /root/.cache/pip/wheels/a0/b0/b2/4f80b9456b87abedbc0bf2d52235414c3467d8889be38dd472
Successfully built bs4
Installing collected packages: soupsieve, beautifulsoup4, bs4
Successfully installed beautifulsoup4-4.8.2 bs4-0.0.1 soupsieve-1.9.5

# python2.7 wia.py
Traceback (most recent call last):
  File "wia.py", line 9, in <module>
    from urllib.request import urlopen
ImportError: No module named request

# python3.8 wib.py
Traceback (most recent call last):
  File "wib.py", line 8, in <module>
    from bs4 import BeautifulSoup
ModuleNotFoundError: No module named 'bs4'

# pip install urllib
ERROR: Could not find a version that satisfies the requirement urllib (from versions: none)
ERROR: No matching distribution found for urllib

# pip3 install urllib
Collecting urllib
Exception:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/usr/lib/python3/dist-packages/pip/commands/install.py", line 353, in run
    wb.build(autobuilding=True)
  File "/usr/lib/python3/dist-packages/pip/wheel.py", line 749, in build
    self.requirement_set.prepare_files(self.finder)
  File "/usr/lib/python3/dist-packages/pip/req/req_set.py", line 380, in prepare_files
    ignore_dependencies=self.ignore_dependencies))
  File "/usr/lib/python3/dist-packages/pip/req/req_set.py", line 554, in _prepare_file
    require_hashes
  File "/usr/lib/python3/dist-packages/pip/req/req_install.py", line 278, in populate_link
    self.link = finder.find_requirement(self, upgrade)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 465, in find_requirement
    all_candidates = self.find_all_candidates(req.name)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 423, in find_all_candidates
    for page in self._get_pages(url_locations, project_name):
  File "/usr/lib/python3/dist-packages/pip/index.py", line 568, in _get_pages
    page = self._get_page(location)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 683, in _get_page
    return HTMLPage.get_page(link, session=self.session)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 795, in get_page
    resp.raise_for_status()
  File "/usr/share/python-wheels/requests-2.18.4-py2.py3-none-any.whl/requests/models.py", line 935, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://pypi.org/simple/urllib/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今日のpython error: ModuleNotFoundError: No module named 'bs4'

プログラムちょい替え(2)Python3: URLをコマンドライン引数で(wikipedia)
https://qiita.com/kaizen_nagoya/items/fc095b0c580a35001ea7

の作業をdockerで実施中。

docker(89) dockerでpython2, python3
https://qiita.com/kaizen_nagoya/items/ecbe11a4d743357134d5

docker/ubuntu
# pip install bs4
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. More details about Python 2 support in pip, can be found at https://pip.pypa.io/en/latest/development/release-process/#python-2-support
Collecting bs4
  Downloading https://files.pythonhosted.org/packages/10/ed/7e8b97591f6f456174139ec089c769f89a94a1a4025fe967691de971f314/bs4-0.0.1.tar.gz
Collecting beautifulsoup4
  Downloading https://files.pythonhosted.org/packages/c5/48/c88b0b390ae1f785942fc83413feb1268a1eb696f343d4d55db735b9bb39/beautifulsoup4-4.8.2-py2-none-any.whl (106kB)
     |################################| 112kB 1.7MB/s 
Collecting soupsieve>=1.2
  Downloading https://files.pythonhosted.org/packages/81/94/03c0f04471fc245d08d0a99f7946ac228ca98da4fa75796c507f61e688c2/soupsieve-1.9.5-py2.py3-none-any.whl
Collecting backports.functools-lru-cache; python_version < "3"
  Downloading https://files.pythonhosted.org/packages/da/d1/080d2bb13773803648281a49e3918f65b31b7beebf009887a529357fd44a/backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl
Building wheels for collected packages: bs4
  Building wheel for bs4 (setup.py) ... done
  Created wheel for bs4: filename=bs4-0.0.1-cp27-none-any.whl size=1273 sha256=79b8b3765197c5d2662611ba7f1199b00147c4741c41f4dc9cdc82d0f08f0609
  Stored in directory: /root/.cache/pip/wheels/a0/b0/b2/4f80b9456b87abedbc0bf2d52235414c3467d8889be38dd472
Successfully built bs4
Installing collected packages: backports.functools-lru-cache, soupsieve, beautifulsoup4, bs4
Successfully installed backports.functools-lru-cache-1.6.1 beautifulsoup4-4.8.2 bs4-0.0.1 soupsieve-1.9.5

# pip3 install bs4
Collecting bs4
  Cache entry deserialization failed, entry ignored
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/10/ed/7e8b97591f6f456174139ec089c769f89a94a1a4025fe967691de971f314/bs4-0.0.1.tar.gz
Collecting beautifulsoup4 (from bs4)
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/cb/a1/c698cf319e9cfed6b17376281bd0efc6bfc8465698f54170ef60a485ab5d/beautifulsoup4-4.8.2-py3-none-any.whl (106kB)
    100% |################################| 112kB 2.4MB/s 
Collecting soupsieve>=1.2 (from beautifulsoup4->bs4)
  Cache entry deserialization failed, entry ignored
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/81/94/03c0f04471fc245d08d0a99f7946ac228ca98da4fa75796c507f61e688c2/soupsieve-1.9.5-py2.py3-none-any.whl
Building wheels for collected packages: bs4
  Running setup.py bdist_wheel for bs4 ... done
  Stored in directory: /root/.cache/pip/wheels/a0/b0/b2/4f80b9456b87abedbc0bf2d52235414c3467d8889be38dd472
Successfully built bs4
Installing collected packages: soupsieve, beautifulsoup4, bs4
Successfully installed beautifulsoup4-4.8.2 bs4-0.0.1 soupsieve-1.9.5

# python2.7 wia.py
Traceback (most recent call last):
  File "wia.py", line 9, in <module>
    from urllib.request import urlopen
ImportError: No module named request

# python3.8 wib.py
Traceback (most recent call last):
  File "wib.py", line 8, in <module>
    from bs4 import BeautifulSoup
ModuleNotFoundError: No module named 'bs4'

# pip install urllib
ERROR: Could not find a version that satisfies the requirement urllib (from versions: none)
ERROR: No matching distribution found for urllib

# pip3 install urllib
Collecting urllib
Exception:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/usr/lib/python3/dist-packages/pip/commands/install.py", line 353, in run
    wb.build(autobuilding=True)
  File "/usr/lib/python3/dist-packages/pip/wheel.py", line 749, in build
    self.requirement_set.prepare_files(self.finder)
  File "/usr/lib/python3/dist-packages/pip/req/req_set.py", line 380, in prepare_files
    ignore_dependencies=self.ignore_dependencies))
  File "/usr/lib/python3/dist-packages/pip/req/req_set.py", line 554, in _prepare_file
    require_hashes
  File "/usr/lib/python3/dist-packages/pip/req/req_install.py", line 278, in populate_link
    self.link = finder.find_requirement(self, upgrade)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 465, in find_requirement
    all_candidates = self.find_all_candidates(req.name)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 423, in find_all_candidates
    for page in self._get_pages(url_locations, project_name):
  File "/usr/lib/python3/dist-packages/pip/index.py", line 568, in _get_pages
    page = self._get_page(location)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 683, in _get_page
    return HTMLPage.get_page(link, session=self.session)
  File "/usr/lib/python3/dist-packages/pip/index.py", line 795, in get_page
    resp.raise_for_status()
  File "/usr/share/python-wheels/requests-2.18.4-py2.py3-none-any.whl/requests/models.py", line 935, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://pypi.org/simple/urllib/

URLlibのエラーは

pip3でurllibをインストール
http://rongonxp.hatenablog.jp/entry/2018/02/01/000816

# pip3 install urllib3
Collecting urllib3
  Downloading https://files.pythonhosted.org/packages/b4/40/a9837291310ee1ccc242ceb6ebfd9eb21539649f193a7c8c86ba15b98539/urllib3-1.25.7-py2.py3-none-any.whl (125kB)
    100% |################################| 133kB 1.7MB/s 
Installing collected packages: urllib3
Successfully installed urllib3-1.25.7
# python3 wib.py
Traceback (most recent call last):
  File "wib.py", line 15, in <module>
    url = "https://ja.wikipedia.org/wiki/" + urllib.parse.quote(args[1])
IndexError: list index out of range

文書履歴(document history)

ver. 0.01 初稿 20191230 午後
ver. 0.02 urllib3 追記 20191230 夜

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

スマホ/PCからラズパイ4にbluetooth 接続する時のメモ

スマホ/PCからラズパイにbluetooth 接続し、ラズパイ側でpython を使った制御を行う時の一通りの手順をまとめてみました。
(外のサイトを参照していて嵌ったところもあったので、改めて。。。)

動作環境

  • Raspberry PI 4 & Rasubian
  • Windows 10

環境のインストール

参考:
https://qiita.com/shippokun/items/0953160607833077163f

# pyBluez の依存パッケージをインストール
$ sudo apt-get install -y python-dev libbluetooth3-dev

# pyBluez のインストール
$ sudo pip3 install pybluez

# sudo apt-get install bluetooth blueman -y # bluez-tool

$ sudo apt install libusb-dev
$ sudo apt install libdbus-1-dev
$ sudo apt install libglib2.0-dev
$ sudo apt install libudev-dev -y
$ sudo apt install libical-dev -y
$ sudo apt install libreadline-dev -y
$ sudo apt install libdbus-glib-1-dev -y
$ sudo apt install libbluetooth-dev

ラズパイのBluetooth macアドレスを確認する(必要に応じて)

$ hciconfig
hci0:   Type: Primary  Bus: UART
        BD Address: DC:A6:32:37:3D:60  ACL MTU: 1021:8  SCO MTU: 64:1
        UP RUNNING PSCAN
        RX bytes:3163 acl:22 sco:0 events:92 errors:0
        TX bytes:3627 acl:21 sco:0 commands:64 errors:0

未使用のチャンネルを調べる(必要に応じて)

$ sudo sdptool browse local | grep Channel
    Channel: 17
    Channel: 16
    Channel: 15
    Channel: 14
    Channel: 10
    Channel: 9
    Channel: 24
    Channel: 12
    Channel: 3

sudo sdptool browse local だけ実行すると何にどのチャンネルが使われているのがわかると思います。

$ sudo sdptool browse local
...
Service Name: Headset Voice gateway
Service RecHandle: 0x10005
Service Class ID List:
  "Headset Audio Gateway" (0x1112)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 12
Profile Descriptor List:
  "Headset" (0x1108)
    Version: 0x0102

...

未使用のチャンネルを指定してシリアルポートサービスを追加。
(当然のことながら、使用済みのチャンネルを指定するとこの後のステップで失敗します。これに気づかずに最初嵌りました…)

sudo sdptool add --channel=22 SP

チャンネルを指定せずにシリアルポートサービスを追加する場合は、上記の代わりに以下を実行する。

sudo sdptool add SP

シリアルポートサービスを追加できたかどうか確認する。

sudo sdptool browse local

上記を実行すると以下のような
Service Name: Serial Port
という出力が得られるはず。ここに
Channel: 1
のようにチャンネル番号が表示されているので確認しておくこと。

Service Name: Serial Port
Service Description: COM Port
Service Provider: BlueZ
Service RecHandle: 0x10001
Service Class ID List:
  "Serial Port" (0x1101)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 1
Language Base Attr List:
  code_ISO639: 0x656e
  encoding:    0x6a
  base_offset: 0x100
Profile Descriptor List:
  "Serial Port" (0x1101)
    Version: 0x0100

設定ファイルを編集して、起動直後にBluetoothでシリアル通信ができるようにする。

sudo nano /etc/systemd/system/dbus-org.bluez.service

ExecStart ... の行に --compat を追記する(互換モードで動作するようにする)。
また、

ExecStartPost=/usr/bin/sdptool add SP
# チャンネルを指定する場合は上記の代わりに以下を記述する。
# 使用済のチャンネルを指定しないように注意。
# ExecStartPost=/usr/bin/sdptool add --channel=22 SP

を追記してシリアル通信プロトコル(SPP)が起動時に追加されるようにしておく。
チャンネル番号の指定は任意で。

[Unit]
Description=Bluetooth service
Documentation=man:bluetoothd(8)
ConditionPathIsDirectory=/sys/class/bluetooth

[Service]
Type=dbus
BusName=org.bluez
ExecStart=/usr/lib/bluetooth/bluetoothd --compat
ExecStartPost=/usr/bin/sdptool add SP
# チャンネルを指定する場合は上記の代わりに以下を記述する。
# 使用済のチャンネルを指定しないように注意。
# ExecStartPost=/usr/bin/sdptool add --channel=22 SP

NotifyAccess=main
#WatchdogSec=10
#Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
LimitNPROC=1
ProtectHome=true
ProtectSystem=full


ラズパイを再起動する。

$ sudo reboot -h

ペアリングする

ラズパイのBluetoothをONし、server側でBluetooth が検索されるのを許可する。

sudo bluetoothctl
[bluetooth] power on
[bluetooth] discoverable on
[bluetooth] agent on
[bluetooth] default-agent

この状態で接続しようとすると以下のようなパスキー確認やサービス認証の確認を求められるので、yes/no を入力する。

参考:
https://qiita.com/oko1977/items/9f53f3b11a1b033219ea

[CHG] Device 80:19:34:31:CD:1E Connected: yes
Request confirmation
[agent] Confirm passkey 291086 (yes/no): yes
Authorize service
[agent] Authorize service 0000110e-0000-1000-8000-00805f9b34fb (yes/no): yes
Authorize service
[agent] Authorize service 0000110d-0000-1000-8000-00805f9b34fb (yes/no): yes
[CHG] Device 80:19:34:31:CD:1E UUIDs: 00001000-0000-1000-8000-00805f9b34fb
...
[CHG] Device 80:19:34:31:CD:1E UUIDs: c7f94713-891e-496a-a0e7-983a0946126e
[CHG] Device 80:19:34:31:CD:1E Connected: no
[CHG] Device 80:19:34:31:CD:1E Connected: yes
[CHG] Device 80:19:34:31:CD:1E Connected: no
[bluetooth]#

ここで、

image.png

上記画像のように、サウンドデバイスとして認識される場合、デバイスとプリンターからraspberrypi を選んで、

  • シリアルポート(SPP)'SerialPort'
  • リモートで制御可能なデバイス
  • リモート制御

以外の項目のチェックを外しておく。
(外しておかないと、サウンドデバイスとして扱われる。筆者の環境では、元々使っていたサウンドデバイスが不活性化して音が鳴らなくなった。。。)

image.png

image.png

ちなみに、「シリアルポート(SPP) 'SerialPort'」が出てこない場合は、序盤の手順で
sdptool add SP
を実行してシリアル通信サービスを有効いるかどうかを再度確認すること。

受信してみる

以下を実行する。

sudo rfcomm listen /dev/rfcomm0

チャンネル番号を指定する場合は以下のように実行する(チャンネル番号22の場合)。

sudo rfcomm listen /dev/rfcomm0 22

別コンソールを立ち上げて、Raspberry Pi側でメッセージを確認するために、/dev/rfcomm0をcatする。

$ sudo cat /dev/rfcomm0

また、さらに別コンソールを立ち上げて
Raspberry Pi上で/dev/rfcomm0デバイスにメッセージをechoしてみる。

$ sudo echo abcd > /dev/rfcomm0

python & bluez で受信してみる

  1 # -*- coding: utf-8 -*-
  2 # Author: Shinsuke Ogata
  3
  4 import sys
  5 import traceback
  6 import time
  7 import bluetooth
  8 import threading
  9
 10 class SocketThread(threading.Thread):
 11     '''
 12     @param client_socket accept の結果返ってきたクライアントソケット.
 13     @param notify_receive シリアル通信で受信したデータを処理する関数・メソッド.
 14     @param notify_error エラー時の処理を実行する関数・メソッド
 15     '''
 16     def __init__(self, server_socket, client_socket, notify_receive, notify_error, debug):
 17         super(SocketThread, self).__init__()
 18         self._server_socket = server_socket
 19         self._client_socket = client_socket
 20         self._receive = notify_receive
 21         self._error = notify_error
 22         self._debug = debug
 23
 24     def run(self):
 25         while True:
 26             try:
 27                 data = self._client_socket.recv(1024)
 28                 if self._receive != None:
 29                     self._receive(data)
 30             except KeyboardInterrupt:
 31                 self._client_socket.close()
 32                 self._server_socket.close()
 33                 break
 34             except bluetooth.btcommon.BluetoothError:
 35                 self._client_socket.close()
 36                 self._server_socket.close()
 37                 if self._debug:
 38                     print('>>>> bluetooth.btcommon.BluetoothError >>>>')
 39                     traceback.print_exc()
 40                     print('<<<< bluetooth.btcommon.BluetoothError <<<<')
 41                 break
 42             except:
 43                 self._client_socket.close()
 44                 self._server_socket.close()
 45                 if self._debug:
 46                     print('>>>> Unknown Error >>>>')
 47                     traceback.print_exc()
 48                     print('<<<< Unknown Error <<<<')
 49                 break
 50
 51 class BluetoothServer(threading.Thread):
 52
 53     '''
 54     @param notify_receive シリアル通信で受信したデータを処理する関数・メソッド.
 55     @param notify_error エラー時の処理を実行する関数・メソッド
 56     @param debug デバッグメッセージを出すときTrue をセット
 57     '''
 58     def __init__(self, notify_receive, notify_error=None, debug=False):
 59         super(BluetoothServer, self).__init__()
 60         self._port =1
 61         self._receive = notify_receive
 62         self._error = notify_error
 63         self._server_socket = None
 64         self._debug = debug
 65
 66     def run(self):
 67         try:
 68             self._server_socket=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
 69
 70             if self._debug:
 71                 print("BluetoothServer: binding...")
 72
 73             self._server_socket.bind( ("",self._port ))
 74
 75             if self._debug:
 76                 print("BluetoothServer: listening...")
 77
 78             self._server_socket.listen(1)
 79
 80             client_socket,address = self._server_socket.accept()
 81
 82             if self._debug:
 83                 print("BluetoothServer: accept!!")
 84             task = SocketThread(self._server_socket, client_socket, self._receive, self._error, self._debug)
 85             task.start()
 86         except KeyboardInterrupt:
 87             if self._debug:
 88                 print("BluetoothServer: KeyboardInterrupt")
 89         except:
 90             if self._debug:
 91                 print('>>>> Unknown Error >>>>')
 92                 traceback.print_exc()
 93                 print('<<<< Unknown Error <<<<')
 94
 95
 96 def receive(data):
 97     print("receive [%s]" % data)
 98
 99 def error(data):
100     print("error")
101
102 if __name__ == '__main__':
103     task = BluetoothServer(receive, error, True)
104     task.start()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonのためにVimにALEを導入するときにハマった点

環境

Windows 10
Vim
Windows Terminal
dein

ハマったこと

vim + dein + aleを導入しようとした。
そこで、ALELintALEFifxを実行してみたがソースコードには何の変化もなかった。
pip installしていて、blackなどはインストールされていた。

3時間ほどかけて見つけた原因は、

image.png
でした。
以下のように変更する必要がありました。
image.png

g:python3_host_progが(windowsの)pathのイメージだったので、ディレクトリ指定だと思い込んでいました...
python.exeのpathを指定しないといけないというお話でした。

参考文献

ALE(on NeoVim)でPythonコードを楽に整形する
NeovimでモダンなPython環境を構築する

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

いつ,どこで,どのファイルを実行したのか自動で記録する

はじめに

日頃からシェルスクリプトとPythonのファイルを実行していると,
いつ,どこで,どのような処理をしたのか,残しておきたくなります.
ここでは,これらを記録するための,私なりの工夫をご紹介します.

その日のための作業用ディレクトリ

まず,その日のための作業用ディレクトリを作成します.
以下ではこれを「DF (Day Folder)」と呼ぶことにします.
ドキュメントの下にその月のディレクトリを作成し,
その中にさらに,その日のためのディレクトリを作成します.
DFの作成は,bashrcに以下のように記載して,自動化しています.

~/.bashrc
export DF=/home/Username/Documents/`date '+%m'`/`date '+%m%d'`
mkdir -p $DF

私はDFを,その日の汎用的なフォルダとして,様々な書類の保存に使用しています.
たとえばあるコードを書き換える際には,更新前のコードを

$ cp main.c $DF

と,念のため保存しています.

ジョブファイルを保存して実行するコマンドの定義

次に,シェルスクリプトとPythonのジョブファイルを実行した際に,
エラーにならなかった場合にそのファイルをDFにコピーし,
実行した時間と実行した場所を記録して,区別できるようにします.
私はジョブファイルのファイル名の先頭に実行した時間を付加し,
ジョブファイルの一番先頭の行に実行した場所を挿入しています.

bashrcに次の項目を追記して,コマンド「ana」を定義します.

~/.bashrc
function ana(){
 DFFILE=$DF/`date '+%H%M%S'`$1
 if [ "'`echo ${1##*.}`'" = "'sh'" ]; then
    sh $1 && cp $1 $DFFILE && sed -i 1i`pwd`$1 $DFFILE

 elif [ "'`echo ${1##*.}`'" = "'py'" ]; then
    python $1 && cp $1 $DFFILE && sed -i 1i`pwd`$1 $DFFILE

 else
    echo "the extension of job file must be 'sh' or 'py' with command ana"
    echo "the extension of input file:'`echo ${1##*.}`'"
 fi
}

anaコマンドの引数は,拡張子が ".sh" もしくは ".py" のジョブファイルです.
引数の拡張子を判別して,sh もしくは python のコマンドを実行します.

$ ana hoge.sh #or hoge.py

具体例

$ cd $DF          #ここではDFで作業することにします.
$ ls                    #".sh" と ".py" の二つのファイルを用意しました.
main.sh hoge.py         
$ cat main.sh       #main.sh を実行すると,
echo "This is a pen"    #"This is the pen" と表示します.                                                                                       
$ ana main.sh           #「ana」で実行してみます.
This is a pen           #たしかにシェルスクリプトが実行されました.
$ ls $DF                # DFの中には,main.shに時間がついたファイルがあります. 
182115main.sh   main.sh     hoge.py
$ head $DF/182115main.sh #このファイルの一行目には,実行した場所が記載されています.
/home/Username/Documents/12/1230main.sh
echo "This is a pen"
$ cat hoge.py       #以下,Pythonでも同様の操作になります.
print("Is this a pen?")
$ ana hoge.py 
Is this a pen?
$ ls $DF/Executed
182115main.sh  182329hoge.py    main.sh     hoge.py
$ head $DF/182329hoge.py
/home/Username/Documents/12/1230hoge.py
print("Is this a pen?")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UbuntuでMeCabのユーザ辞書に単語を追加してPythonで使えるようにする

はじめに

最近、研究でpythonとMeCabを使用した解析し始めたものの、単語をユーザ辞書に追加する際に苦労したので自分用にまとめてみました。

環境

  • Ubuntu 16.04
  • Python 3.6 (anacondaの仮想環境)
  • MeCab 0.996 (pip install mecab-python3で導入)

1. 辞書を用意する

辞書はcsvファイルで作成しておきます。辞書のフォーマットは
表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原型,読み,発音
の順で並べます。

vim add_term.csv
アナと雪の女王,,,1,名詞,一般,*,*,*,*,アナと雪の女王,アナトユキノジョオウ,アナトユキノジョオー

左文脈IDと右文脈IDは空欄にしておくと自動で入力してくれます。また、コストはその単語がどれだけ出現しやすいかを示しており、小さいほど出現しやすいという意味になります。コストの推定法もあるようですが、今回は1にしました。不要なものは「*」でオーケーです。
※文字コードは必ず「UTF-8」で作成すること!「Shift-jis」や「EUC-jp」にすると上手くいきませんでした。

2. ユーザ辞書を作成する

作成したcsvファイルからユーザ辞書を作成します。辞書の作成にはMeCabをインストールした際に付属のmecab-dict-indexを使います。

#ユーザ辞書保存先ディレクトリの作成
mkdir /usr/local/lib/mecab/dic/userdic

#辞書作成
sudo /usr/lib/mecab/mecab-dict-index \
-d /usr/local/mecab/dic/ipadic \
-u /usr/local/lib/mecab/dic/userdic/add.dic \
-f utf-8 \
-t utf-8 \
add_term.csv

オプションは以下の通りです。
-d システム辞書が入っているディレクトリ
-u ユーザ辞書の保存先
-f csvファイルの文字コード
-t ユーザ辞書の文字コード
csvファイル

mecab-dict-indexはフルパスで実行します。この時も文字コードはUTF-8を指定します。

reading add_term.csv ... 1
emitting double-array: 100% |###########################################|

done!

と表示されると成功です。

3. MeCabの設定ファイルに作成したユーザ辞書を追加する

設定ファイルに以下の文を追記します。

sudo vim /etc/mecabrc
userdic = /usr/local/lib/mecab/dic/userdic/add.dic

公式サイトでは
/usr/local/lib/mecab/dic/ipadic/dicrc
/usr/local/etc/mecabrc
のどちらかに追記するように書いてありますが、筆者の環境では上手くいかず、上記場所にmecabrcがあったのでそちらに追記することで正しく動作しました。複数辞書を登録したい場合は,

userdic = AAA.dic,BBB.dic

とすれば登録できました。

動作確認

  • コマンドラインから確認する
#追加前
mecab
アナと雪の女王
アナ  名詞,一般,*,*,*,*,アナ,アナ,アナ
と 助詞,並立助詞,*,*,*,*,と,ト,ト
雪 名詞,一般,*,*,*,*,雪,ユキ,ユキ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
女王  名詞,一般,*,*,*,*,女王,ジョオウ,ジョオー
EOS

#追加後
アナと雪の女王
アナと雪の女王   名詞,一般,*,*,*,*,アナと雪の女王,アナトユキノジョオウ,アナトユキノジョオー
EOS
  • pythonのMeCabで使用する
python3
>>> import MeCab
>>> m_t = MeCab.Tagger('-Ochasen \
                        -u /usr/local/lib/mecab/dic/userdic/add.dic')
>>> txt = 'アナと雪の女王を観に行こう。'
>>> print(m_t.parse(txt))
アナと雪の女王    行こ  

インストール済みのmecab-ipadic-neologdと一緒に使いたい場合は

python3
>>> import MeCab
>>> m_t = MeCab.Tagger('-Ochasen \
                        -d /usr/lib/mecab/dic/mecab-ipadic-neologd \
                        -u /usr/local/lib/mecab/dic/userdic/add.dic')

と変更すれば同時に読み込んでくれます。

結論

試行錯誤しましたが、python上で上手く動作するところまで確認できました。もし間違っている点などありましたらご指摘をいただけますと幸いです。

参考サイト

単語の追加方法
MeCab ユーザ辞書への単語追加

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

python DSのデバッグ

◆pdb
デフォルトのデバッグ機能

◆helpコマンド
help(なんかオブジェクト)でだいたいの説明が出てきます

◆対話的なデバッグも
https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/#b31

◆言語ごとのDumpの違い
https://hydrocul.github.io/wiki/programming_languages_diff/io/dump.html
pprintがpythonでは紹介されてます。

ーーー継続して書いていきますーーー

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

第7章 [誤差逆伝播法] P275~(中盤) 【Pythonで動かして学ぶ! あたらしい機械学習の教科書】

誤差逆伝播法(バックプロパゲーション)

この誤差逆伝播法では、ネットワークの出力で生じる誤差(教師信号との差)の情報を使って、出力層の重み$v_{kj}$から中間層への重み$w_{ji}$へと入力法こと逆向きに重みを更新していくことから、この名前がつけられている。
しかし、この誤差逆伝播法は勾配法そのもので、勾配法をフィードフォワードニューラルネットに適用すると誤差逆伝播ほうが自然と導出される。

クラス分類をさせるので、誤差函数には式7-18の平均交差エントロピー誤差を考える(P266)
$$
E(w,v)=-\frac{1}{N}\sum_{n=0}^{N-1}\sum_{k=0}^{K-1}t_{nk}log(y_{nk})\hspace{40pt}(7-18)
$$

・ここでやりたいことは$\partial{E}/ \partial{w_{ji}}$(平均交差エントロピー誤差)を求めたい。
・データが1つの時の交差エントロピー$E_n$を考えたときに、その微分は$\partial{E_n} /
\partial{w_{ji}}$で求まるから、本命の$\partial{E}/ \partial{w_{ji}}$(平均交差エントロピー誤差)はデータの個数分の$\partial{E_n} / \partial{w_{ji}}$を求めてそれを平均すれば良い
ここでtはt=[0,0,1]とかで表していた、教師信号(なんのクラスを表すかの信号)

それが↓
$$
\frac{\partial{E}}{\partial{w_{ji}}}=\frac{\partial}{\partial{w_{ji}}}\frac{1}{N}
\sum_{n=0}^{N-1}E_n
=\frac{1}{N}\sum_{n=0}^{N-1}
\frac{\partial{E_n}}{\partial{w_{ji}}}
$$

今回はD=2,M=2,K=3の場合を考える。

D:入力値の個数
M:中間層の個数
K:出力の個数
w,v:重み

重みは、w,vの二つあるので、まず$E_n$を$v_{kj}$で偏微分した式を求める。その次に$E_n$を$w_{ji}$で偏微分した式を求める。(順番に特に意味はなさそう。誤差逆伝播法では、誤差を元に逆順に式展開が伝播するため逆から行っている。)

$
\frac{\partial{E}}{\partial{v_{kj}}}
$
の$E_n$の部分は7-22の$E_n(w,v)=-\sum_{k=0}^{K-1}t_{nk}log(y_{nk})$なので、$\frac{\partial{E}}{\partial{v_{kj}}}$これは連鎖律で偏微分を求めることができる。

連鎖律↓
$$
\frac{\partial{E}}{\partial{v_{kj}}}=
\frac{\partial{E}}{\partial{a_k}}\frac{\partial{a_k}}{\partial{v_{kj}}}
$$

$a_k$は入力値とダミー変数の総和

ここでは、まず左側の$\frac{\partial{E}}{\partial{a_k}}$から考える。
Eの部分は$E_n$が省略されているものなので、ここを式7-22で置き換えると、(k=0の場合で考える[データ数0個?])

$$
\frac{\partial{E}}{\partial{a_0}}= \
\frac{\partial}{\partial{a_0}}(-t_0logy_0-t_1logy_1-t_2logy_2)
$$

となる。
ここで対数の微分の公式を使うと
・説明にあるようにtは教師信号でyは入力総和の出力なので、$a_0$と関係があるため。
ここでtはt=[0,0,1]とかで表していた、教師信号(なんのクラスを表すかの信号)

対数の微分:
$
\begin{align*} (\log x)' = \frac{1}{x} \end{align*}
$

と表せる↓
$$
\frac{\partial{E}}{\partial{a_0}}=\
-t_0\frac{1}{y_0}\frac{\partial{y_0}}{\partial{a_0}}
-t_1\frac{1}{y_1}\frac{\partial{y_1}}{\partial{a_0}}
-t_2\frac{1}{y_2}\frac{\partial{y_2}}{\partial{a_0}}
\hspace{40pt}(7-27)
$$

第7章(前半)で説明したように$\partial{y_0} / \partial{a_0}$の部分ではソフトマックス関数が使用されている。
image.png

そこで、4-130で導いた、ソフトマックス関数の偏微分の公式通りにすると、
公式4-130:
$\frac{\partial{y_j}}{\partial{x_i}}=y_j(l_{ij}-y_i)$
・iは入力値の係数、jは出力値の係数
ここで、lは$i=j$のとき1、$i\neq{j}$の時0

式7-28のようになります。
$$
\frac{\partial{y_0}}{\partial{a_0}}=y_0(1-y_0)
$$
今回はダミー変数を含めるとM=K=3なのでlの部分は1

残りの二つは、入力値の係数と、出力値の係数が異なるのでそれぞれ
$$
\frac{\partial{y_1}}{\partial{a_0}}=-y_0y_1
$$
$$
\frac{\partial{y_2}}{\partial{a_0}}=-y_0y_2
$$
となる。

それぞれ3つを代入すると
よって、式7-27は以下のようになる(7-31):

\begin{align}
\frac{\partial{E}}{\partial{a_0}}&=
-t_0\frac{1}{y_0}\frac{\partial{y_0}}{\partial{a_0}}
-t_1\frac{1}{y_1}\frac{\partial{y_1}}{\partial{a_0}}
-t_2\frac{1}{y_2}\frac{\partial{y_2}}{\partial{a_0}}\\
&=-t_0(1-y_0)+t_1y_0+t_2y_0\\
&=(t_0+t_1+t_2)y_0-t_0\\
&=y_0-t_0
\end{align}

最後は$t_0+t_1+t_2=1$を使った。

$y_0$が1番目のノードのニューロンの出力で、$t_0$がそれに対する教師信号なので$y_0-t_0$は誤差を表している。

同じようにk番目のデータ(k=1,2)を考えると式(7-32)のようになる。

そして、式7-25の連鎖律の左側の部分は7-33のように表せる。(偏微分左側)

式7-25の $\partial{a_k} / \partial{v_kj}$ の部分を考える。
k=0の場合を考えると、総和の$a_0$は中間層の出力(z)と中間層から出力層への重みvの総和なので、
$$
a_0=v_{00}z_0+v_{01}z_1+v_{02}z_{02}
$$

なので、$a_0$をそれぞれのv($v_0,v_1,v_2$)について解くと7-37のようになる。

それで、7-37をまとめて書くと、7-38にになる。

k=1,k=2の場合でも同様な結果が得られるので、全てまとめて式7-39のようになる
$$
\frac{\partial{a_k}}{\partial{v_{kj}}}=z_j\hspace{40pt}(7-39)
$$

これで偏微分の左側、右側が揃ったので、組み合わせると式7-40のようになる。

image.png

式7-41で言っていることは、今欲しい値は$v_{kj}$という適切な重みの値が欲しいので、その値を適切に調整する時の理論が図で表していること。
・zは(シグモイドを通しているから)0~1の値をとる確率で出力yと教師信号tの誤差分をなくすようなvになりたい分けだから、誤差($\delta_k^{(2)}$のぶんだけvの値を変更する。)
・誤差($\delta_k^{(2)}$)が0の場合 = 出力$y_k$と目標データ(教師信号)$t_k$ が一致していれば,($y_k-t_k=0$)変更分の$-\alpha\delta_k^{(2)}z_j$は0となる。

(重要)P281:

目標データ$t_k$が0なのに、出力$y_k$が0よりも大きかった場合、誤差$\delta_k^{(2)}=(y_k-t_k)$は正の値となります。$z_j$は常に正のですから、結果、$-\alpha\delta_k^{(2)}z_j$は負の数となり、$v_{kj}$は減る方向へ修正される。つまり、出力が大きすぎて誤差が生じたので、ニューロン$z_j$からの影響を絞る方向へ重みが変更されると解釈できます。また、入力$z_j$が大きかったら、その結合からの出力への寄与が大きかったことになるので、$v_{kj}$の変更量もその分大きくするようになっていると解釈できます。

ここの部分が非常に重要で、ここで言っていることは、
・目的データtよりも出力データyの方が大きければ、vの変更はその出力yを減らす方に変化する(v=0.2とかになるのかな?)
・そして、誤差(y-t=0)がなければ、vは変わらない
・また、入力値z(これは確率を表す)が大きければvの変更量もその分大きくなる

Eのwで微分を求める。

$\partial{E}/\partial{w_{ji}}$を求める

・式7-43は7-34と同じ
・式7-44は7-39と同じ
・なので、同様に式7-45は7-41と同じ
として処理できる。

$\delta_{j}^{(1)}$をとりあえずとしておいたで、それが何かを求める。

image.png

まず、最初に7-43の式を連鎖律で偏微分すると:
$$
\delta_j^{(1)}=\frac{\partial{E}}{\partial{b_j}}=
\biggl(
\sum_{k=0}^{K-1}\frac{\partial{E}}{\partial{a_k}}
\frac{\partial{a_k}}{\partial{z_j}}
\biggr)
\frac{\partial{z_j}}{\partial{b_j}}
$$
なるのを理解するのに、以下を理解する必要がある。image.png

・ここでは、$g_0$と$g_1$が$w_0$とw_1$の関数で、fがg_0$とg_1$の関数となっている場合となっているので、これを置き換える。

$$
E(a_k(z_0,z_1))
$$
(k=0~2の3つ)

なので、(4-62)が適用できる。
という関数になる。
$$
\frac{\partial}{\partial{z_j}}E(a_0(z_0,z_1),a_1(z_0,z_1),a_2(z_0,z_1))=\sum_{k=0}^{K-1}\frac{\partial{E}}{\partial{a_k}}\frac{\partial{a_k}}{\partial{z_j}}
$$
となる。

これを組み合わせると、式7-46のように展開できる。

式7-47がなぜ$v_{kj}$になるかわかりませんが、P283で説明している通り、$\delta_{j}^{(1)}$は以下のようになる
・式7-47では、$z_j$について偏微分しろと言っているので、それ以外のvは定数とおく、z=1として残るところのvだけ定数も残るので、$v_{kj}$が残る。

それらを合わせると↓

$$
\delta_{j}^{(1)}=h'(b_j)\sum_{k=0}^{K-1}v_{kj}\delta_{k}^{(2)}
$$

P284~ 誤差逆伝播法まとめ

誤差逆伝播法で行いたいことは、w,vなどの重みパラメータの最適化であり、逆向きに伝播させることでそれを可能にしている。

・最初の$h'(b_j)$は、シグモイド関数で変換されたものなので、$b_j$は0~1の値をとっている、そのため、この値は常に正の値をとる。
・$\frac{\partial{E}}{\partial{a_k}}=
\delta_{k}^{(2)}=
(y_k-t_k)h'(a_k)$なので、出力層の出力yに重みvをかけて集めてきているものです。(出力層での誤差が大きければ、v*(大きい値)となり、vが大きく働く。) 

①では、まず、適当にw,vを設定して、出力yを得る。
②教師信号のtがあるので、そのtとyを比較する。
③今まで導出した式を用いて誤差を逆向きに伝播する。具体的には全ての誤差(y-t)と重みv(2層目)をかけて総和をとり、それを中間層のシグモイド関数を通した総和$h'(b_0)$をかける。(それが③の図の式)
④誤差が0になるようにw,vを更新する。(誤差がわかったので、その分が0になるようにする。)

image.png
image.png

 誤差逆伝播法式まとめ

image.png

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

文書のベクトル化を応用して街の「地域色」を可視化する

概要

自然言語処理における文書ベクトル化の手法を応用して、テキストで記述された地域情報をもとに各地域を色分けした地図を作成する、ということを行いました。
具体的には、首都圏の都市計画(整備、開発、保全の方針:いわゆる整開保)の文書をトピックモデル(LDA)で分類した結果を、RGB値で地図上に表現するというものです。
sample_20191230.png

これにより、「この地域とあの地域は地理的には離れているけど意外と同じ様な特性がある」といった概念的な発見に繋げられないか、と考えて実施しています。

現状はデータが整理しきれておらず歯抜け状態ですが、例えば茨城県は紫色っぽいのが多いのに対し、千葉県・埼玉県は緑・茶系が多い。しかし、埼玉でも秩父など西側は茨城県と近い色を示しています。また君津と所沢なども意外と近そう。なぜ埼玉市の中心(大宮)も紫なのかは謎、、などなど。データ数が増えれば精度の納得感は向上するかもしれません。

元のデータ

国土交通省都市計画マスタープランリンク集から千葉・埼玉・茨城の都市計画文書を取り繕ってきました。ただし、全ての地域は網羅できておらず、あくまでサンプルとなります。こんな感じのデータセット(CSV形式)をインプットとして作成しました。

name description
1 竜ヶ崎・牛久 竜ヶ崎・牛久都市計画(龍ケ崎市,牛久市,利根町)都市計画区域の〜(略)〜近隣都市間の連携を強化し,豊かな自然・田園環境と共生しながら,職・住が一体となった〜 (略)
2 飯能市 飯能都市計画(飯能市)都市計画区域の〜(略)〜公共交通の利用促進やみどりの創出などにより、低炭素社会の実現を図る。地域の個性ある発展〜 (略)

文書のベクトル化

  • 実際に作成したコードは下記となります。MeCabで形態素解析、gensimでトピックモデルによるベクトル化、scikit-learnでTSNEによる次元削減を行っています。改善点は多いかもですが、その辺りは随時修正したいと思います。

    • 実行環境>> OS: MacOS Catalina | 言語: python3.6.6
visualizer.py
from sklearn.manifold import TSNE
from gensim import corpora, models
import string
import re
import MeCab
import pandas as pd
import numpy as np

#Text Tokenizer by MeCab
def text_tokenizer(text):
    token_list = []
    tagger = MeCab.Tagger()
    tagger.parse('') 
    node = tagger.parseToNode(text)
    while node:
        pos = node.feature.split(",")
        if pos[0] in ["名詞", "動詞", "形容詞"]: #target word-class
            if pos[6] != '*': #lemma is added when it exists
                token_list.append(pos[6])
            else:
                token_list.append(node.surface)        
        node = node.next
    return list(token_list)

#Loading input dataset
df = pd.read_csv('input.csv', encoding="utf-8")
df['text'] = df['description'] #set target column

#Remove https-links
df['text_clean'] = df.text.map(lambda x: re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-]+', "", x))
#Remove numerals
df['text_clean'] = df.text_clean.map(lambda x: re.sub(r'\d+', '', x))
#Converting all letters into lower case
df['text_clean'] = df.text_clean.map(lambda x: x.lower())
#Creating DataFrame for Token-list
df['text_tokens'] = df.text_clean.map(lambda x: text_tokenizer(x))

#LDA
np.random.seed(2000)
texts = df['text_tokens'].values
dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
ldamodel = models.ldamodel.LdaModel(corpus, id2word=dictionary, num_topics=20, passes=5, minimum_probability=0)
ldamodel.save('lda_bz.model')
print(ldamodel.print_topics())

#Converting Topic-Model result into numpy matrix
hm = np.array([[y for (x,y) in ldamodel[corpus[i]]] for i in range(len(corpus))])

#Dimensionality reduction by tsne
tsne = TSNE(n_components=3, init='pca', verbose=1, random_state=2000, perplexity=50, method='exact', early_exaggeration=120, learning_rate=200, n_iter=1000)
embedding = tsne.fit_transform(hm)

x_coord = embedding[:, 0]
y_coord = embedding[:, 1]
z_coord = embedding[:, 2]

#RGB conversion with normalization
def std_norm(x, axis=None):
    xmean = x.mean(axis=axis, keepdims=True)
    xstd = np.std(x, axis=axis, keepdims=True)
    y = (x-xmean)/xstd
    min = y.min(axis=axis, keepdims=True)
    max = y.max(axis=axis, keepdims=True)
    norm_rgb = (y-min)/(max-min) * 254
    result = norm_rgb.round(0)
    return result

x_rgb = std_norm(x_coord, axis=0)
y_rgb = std_norm(y_coord, axis=0)
z_rgb = std_norm(z_coord, axis=0)

embedding = pd.DataFrame(x_coord, columns=['x'])
embedding['y'] = pd.DataFrame(y_coord)
embedding['z'] = pd.DataFrame(y_coord)
embedding["r"] = pd.DataFrame(x_rgb)
embedding["g"] = pd.DataFrame(y_rgb)
embedding["b"] = pd.DataFrame(z_rgb)
embedding['description'] = df.description

#export to csv
embedding.to_csv("output.csv", encoding="utf_8")

  • 基本的なコンセプトは、自然言語処理においてよく行われる”文書ベクトルの3次元可視化”を実施し、その3次元ベクトル(x,y,z値)をRGB値に変換しています。

  • 今回、文書のベクトル化ではトピックモデルを採用しています。その理由として、その他のベクトル化手法(例えばtf-idfやword2vecベース)では次元削減後のxyz値に外れ値が出てくるケースが多く、0〜255のRGB値に変換した際にその影響が大きすぎたためです。トピックモデルの場合、パラメータを適切に設定すれば外れ値が生じにくく、この問題をクリアできると考えました。

地図上での可視化

  • 文書情報をRGB値に変換したあとは、各市町村のRGB値をマップ上で可視化します。
    私の場合、QGISで地理情報との紐付けを行い、Leafletを使って描画しました。こちらは今回は省略しますが要すれば別記事にて詳述します。

  • QGISで属性情報にあるRGB値をそのまま色で表現する方法があるのか、どなたかご存知であれば教えてください。

留意点と今後の課題

  • 文書ベクトルは大まかに言って、語られている内容(単語の出現傾向やトピックなど)が近ければベクトルとしても近い距離に現れるため、それを色に変換すれば、似た様な色合いの街は語られている内容も近いと判断できるかなと考えています。このあたり、もしカウンターパンチがあれば謹んでお受け致しますのでご指摘ください。

  • この色は地域同士の相対的な関係性を示すため、初期値を変えて解析をするとその度に色が変わります。「毎年この作業を実施して経年で変化を追う」といった場合に使いづらいので、このあたりの解決策も模索したい。

  • 今回とりあえず試験的に都市計画文書を用いていますが、まだ全ての市町村をカバーできていないことだけでなく、それ以前にこれらが「街の特色」を表しているかというと、一般的な感覚とは乖離があると思います。ゆくゆくは、観光客の声や住民参加型ワークショップの協議録などを反映させたものができたら面白いと思っています。

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

年賀状作成をPythonに任せたかった

年末

僕自身は年賀状を毎年作成するのを諦めたのですが、アルバイトで家族の分の年賀状を毎年パソコンでつくっています。毎年この時期になるとExcelとワードの差し込み印刷と格闘しながら年賀状を作っているのですが...

  • Excelの関数ワカラーン、せっかく勉強してるしPandasでやらせてほしい。
  • 差し込み印刷の柔軟さが微妙、例えば差出人とかも連名で場合分けしたい

...というわけで、Pythonで年賀状作成をしてみることにしました。

やろうとしたこと

  • Pandasで住所録データの前処理
  • OpenCVではがきの印刷位置指定 (住所とか郵便番号とか)
  • Pillowで宛名面・通信面作成
  • 印刷 (失敗)

住所録データの前処理

ダミーデータ

まず、実験用にダミーの住所録データを作ります。
https://yamagata.int21h.jp/tool/testdata/
こちらで50人分のアドレスを作り、CSVにしました。

import pandas as pd
df = pd.read_csv("address.csv")
df.columns = ["index", "name", "address"]

スクリーンショット 2019-12-30 15.39.09.png

名前と住所の分割

次に、今後の連名対応の為、氏名を姓と名に分けます。また、住所をいい感じに段組するために住所を2分割します。具体的には

  1. 建物・マンション名がない場合→住所の数字以降 (丁目, 番地など) で分割
  2. 建物・マンション名がある場合→建物・マンション名で分割

という感じで分けていきます

df["sei"]=df["name"].str.split(expand=True)[0]
df["mei"]=df["name"].str.split(expand=True)[1]

import re
def split_address(address):
    if " " in address:
        pos = int(address.find(" "))
        return [address[:pos],address[pos:]]
    else:
        pos = int(re.search("\d", address).start())
        return [address[:pos],address[pos:]]

df["address_1"] = df.address.apply(lambda x: split_address(x)[0])
df["address_2"] = df.address.apply(lambda x: split_address(x)[1])

先にdataframeの列を初期化してれば、二列いっぺんに代入できるらしいですね

郵便番号取得

こちらの郵便番号取得APIを使います。map関数を使うと続々とリクエストを送ってしまいますので、適度にsleepを挟みながら取得していきます。

import json
import requests
import time

def get_postal(address):
    pos = int(re.search("\d", address).start())
    address = address[:pos]
    try:
        res = requests.get("http://geoapi.heartrails.com/api/json?method=suggest&matching=prefix&keyword="+address)
        postal = json.loads(res.text)["response"]["location"][0]["postal"]
        time.sleep(1)
        return postal
    except:
        return ""

df["postal"] = df["address"].map(lambda x: get_postal(x))

最終的にこんな感じになります。
スクリーンショット 2019-12-30 15.48.59.png

get_postal()は例外が発生した場合、空の郵便番号を返すようになっています。リクエストの送りすぎの可能性もありますが、住所の妥当性チェックもできると思います。

はがきの印刷位置指定

こんな感じにはがきを一枚用意し、住所とかを印刷したい箇所を黒塗りにし、雑に写真を撮ります。

nengajo.jpg

アフィン変換

これをまずアフィン変換による台形補正で、はがきを綺麗に抜き出します。コードはこちらのサイトのコードをほぼそのまま使わせていただきました。

まず輪郭抽出

img = np.array(Image.open("nengajo.jpg"))

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,128,255,cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh , cv2.RETR_TREE, cv2.RETR_LIST)

new_img = img.copy()
plt.imshow(cv2.drawContours(new_img, contours, -1, (128,0,0), 30))

スクリーンショット 2019-12-30 15.58.02.png

次に、一番 (面積の) 大きな輪郭の四隅を抽出します。

menseki=[ ]

for i in range(0, len(contours)):
    menseki.append([contours[i],cv2.contourArea(contours[i])])

menseki.sort(key=lambda x: x[1], reverse=True)

epsilon = 0.1*cv2.arcLength(menseki[0][0],True)
approx = cv2.approxPolyDP(menseki[0][0],epsilon,True)

new_img = img.copy()
plt.imshow(cv2.drawContours(new_img, approx, -1,(0, 0, 255),100))

スクリーンショット 2019-12-30 15.59.13.png

最後に、はがきを真正面から見るようにアフィン変換し、切り抜きます。また、はがきの推奨ピクセルサイズが2362x3496pxということなので、それに合わせます。

approx=approx.tolist()

left = sorted(approx,key=lambda x:x[0]) [:2]
right = sorted(approx,key=lambda x:x[0]) [2:]

left_down= sorted(left,key=lambda x:x[0][1]) [0]
left_up= sorted(left,key=lambda x:x[0][1]) [1]

right_down= sorted(right,key=lambda x:x[0][1]) [0]
right_up= sorted(right,key=lambda x:x[0][1]) [1]

perspective1 = np.float32([left_down,right_down,right_up,left_up])
perspective2 = np.float32([[0, 0],[1378, 0],[1378, 2039],[0, 2039]])

psp_matrix = cv2.getPerspectiveTransform(perspective1,perspective2)
img_psp = cv2.warpPerspective(img, psp_matrix,(1378,2039))

plt.imshow(img_psp)

スクリーンショット 2019-12-30 16.01.03.png

綺麗に抜き出せましたね、すごい!

塗りつぶし領域の抽出

OpenCVのcv2.threshold()により画像の閾値処理をし、塗りつぶした領域をマスクします。cv2.threshold()はグレースケールの画像を投げる事もできますが、HEIGHT×WIDTHの形の、uint8のnumpy行列ならなんでも投げられます。また、自分の経験上、閾値処理はBGRでやるよりもHSVのどれかに注目してやるとうまくいくことが多いです。
今回画像をHSVの3チャンネル画像HEIGHT×WIDTH×3にまず変換します、そのうち、V成分のチャンネルのみを抜き出し、HEIGHT×WIDTHの行列を閾値処理する事で、うまく塗りつぶした部分を抽出することができました。

_, v_img = cv2.threshold(cv2.cvtColor(img_psp,cv2.COLOR_BGR2HSV)[:,:,2], 128, 255, cv2.THRESH_BINARY)
plt.imshow(v_img)

スクリーンショット 2019-12-30 16.08.09.png

ここで、マスクの輪郭を抽出します。輪郭を抽出する前に、マスク自体のノイズをまずモルフォロジー変換で除去します。また、輪郭の面積が一定以下のものをリスト内包表記でフィルタしています。

_, v_img = cv2.threshold(cv2.cvtColor(img_psp,cv2.COLOR_BGR2HSV)[:,:,2], 128, 255, cv2.THRESH_BINARY)

kernel = np.ones((8, 8),np.uint8)
v_img = cv2.morphologyEx(v_img, cv2.MORPH_CLOSE, kernel)


contours, hierarchy = cv2.findContours(v_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

menseki=[ ]

for i in range(0, len(contours)):
    menseki.append([contours[i],cv2.contourArea(contours[i])])

menseki = [m[0] for m in menseki if m[1] > 800]

contours=menseki

cont_vis = cv2.drawContours(v_img, contours, -1, (128,0,0), 30)
for idx, con in enumerate(contours):
    cont_vis = cv2.putText(cont_vis, str(idx), (con[2][0][0], con[2][0][1]), cv2.FONT_HERSHEY_PLAIN, 3, (64, 64, 64), 5)

plt.figure(figsize=(15,20))
plt.imshow(cont_vis)

輪郭抽出の結果はこんな感じ

スクリーンショット 2019-12-30 16.11.34.png

ここで、モルフォロジー変換をかけない結果はこんな感じになります。

スクリーンショット 2019-12-30 16.11.20.png

今回輪郭の誤検知はあまりありませんが、マスクに乗っている細かいノイズが除去されていることがわかります。

あとはこの画像を見ながら、どのデータをどこに配置したいか定義します。

address_conts = {
    "to_postal":list(map(cv2.boundingRect, [contours[17],contours[18],contours[16], contours[14], contours[12], contours[13], contours[15]])),
    "to_address1":cv2.boundingRect(contours[10]),
    "to_address2":cv2.boundingRect(contours[9]),
    "to_name":cv2.boundingRect(contours[11]),
    "from_address":cv2.boundingRect(contours[8]),
    "from_name":cv2.boundingRect(contours[7]),
    "from_postal":list(map(cv2.boundingRect, [contours[6],contours[0],contours[2], contours[3], contours[5], contours[4], contours[1]])),
}

cv2.bouundingRect()で輪郭を外接する長方形で囲い、xywh座標 ($(x,y)$は長方形の左上の座標, $(w,h)$は長方形のの縦横の長さ) で保持します。

この結果を見て思ったのですが、cv2.findContours()って画像の下にあるものから輪郭を検出してるんですかね...

印刷面の作成

Pillowで印刷面を作っていきます。

宛名面

先ほどの座標をもとに、文字を縦書きで置いていきます。とりあえず今回は住所録から一件だけ取り出して作ってみます。「Pillow 縦書き」とかでググるとこちらの記事が出てくるのですが、この記事の方のおかげで、Pillowのv6以上ではttb指定で縦書きが可能になっています。あとはraqmの環境構築が必要です。フォントの.otfファイルは今回の実行ディレクトリ (jupyter labを使いました) におきます。

def calc_start_pos(cont):
    x = cont[0]
    y = cont[1]
    return (x, y)

from PIL import Image, ImageDraw, ImageFont

data = df.loc[2]

fnt1 = ImageFont.truetype("GenEiAntique.otf", 80)
fnt2 = ImageFont.truetype("GenEiAntique.otf", 150)
fnt3 = ImageFont.truetype("GenEiAntique.otf", 50)


address = Image.new('RGBA', (v_img.shape[1],v_img.shape[0]), (255,255,255,255))
d = ImageDraw.Draw(address)
d.text(calc_start_pos(address_conts["to_address1"]), data["address_1"].replace("-","|"), font=fnt1, fill=(0,0,0,255), direction="ttb")
d.text(calc_start_pos(address_conts["to_address2"]), data["address_2"].replace("-","|"), font=fnt1, fill=(0,0,0,255), direction="ttb")
d.text(calc_start_pos(address_conts["to_name"]), data["name"] + " 様", font=fnt2, fill=(0,0,0,255), direction="ttb")
d.text(calc_start_pos(address_conts["from_address"]), "東京都新宿区歌舞伎町127-0-1".replace("-","|"), font=fnt3, fill=(0,0,0,255), direction="ttb")
d.text(calc_start_pos(address_conts["from_name"]), "某山某太郎", font=fnt1, fill=(0,0,0,255), direction="ttb")
for num, rect in zip(data["postal"], address_conts["to_postal"]):
    d.text(calc_start_pos(rect), num, font=fnt1, fill=(0,0,0,255), direction="ttb")
for num, rect in zip("1600021", address_conts["from_postal"]):
    d.text(calc_start_pos(rect), num, font=fnt1, fill=(0,0,0,255), direction="ttb")

calc_start_pos()で文字を書き始める位置を計算しようと思ったのですが、先ほど抽出した矩形の左上座標をそのまま指定すればうまく配置できました。

こんな感じで描いた宛名面を、先ほどのはがきに載せてみます。あらかじめ黒く塗り潰したところはマスクを使い、はがきの色っぽく塗り戻しています。

address_array = np.array(address)

final_image = np.zeros_like(img_psp)
letter_image = img_psp.copy()

for i in range(3):
    letter_image[:,:,i] = np.where(
        v_img < 129,
        222,
        img_psp[:,:,i]
    )

for i in range(3):
    final_image[:,:,i] = np.where(
        address_array[:,:,i] ==0,
        address_array[:,:,i],
        letter_image[:,:,i]
    )

plt.figure(figsize=(7,10))
plt.imshow(final_image)

スクリーンショット 2019-12-30 16.29.59.png

細かい調整はまだできそうですが、そこそこいい感じになりましたね。np.where()はマスクを元にあれこれ処理するときに便利です。

裏面

いい感じにOpenCVで書きます。

ura = np.zeros_like(final_image)+255
pts = np.array([[400,500],[1100,500],[750,1500]], np.int32)
pts = pts.reshape((-1,1,2))
ura = cv2.fillPoly(ura,[pts],(128,128,128))
ura = cv2.circle(ura,(400,500), 200, (72, 72, 72), -1)
ura = cv2.circle(ura,(1100,500), 200, (72, 72, 72), -1)
ura = cv2.circle(ura,(750,1500), 50, (72, 72, 72), -1)
ura = cv2.circle(ura,(750,1500), 50, (72, 72, 72), -1)
ura = cv2.circle(ura,(670,900), 50, (72, 72, 72), -1)
ura = cv2.circle(ura,(850,900), 50, (72, 72, 72), -1)
ura = cv2.line(ura,(750,1500),(900,1400),(72, 72, 72),20)
ura = cv2.line(ura,(750,1500),(900,1500),(72, 72, 72),20)
ura = cv2.line(ura,(750,1500),(900,1600),(72, 72, 72),20)
ura = cv2.line(ura,(750,1500),(600,1400),(72, 72, 72),20)
ura = cv2.line(ura,(750,1500),(600,1500),(72, 72, 72),20)
ura = cv2.line(ura,(750,1500),(600,1600),(72, 72, 72),20)
ura = cv2.putText(ura, "A Happy New Year!!", (50, 1800), cv2.FONT_HERSHEY_PLAIN, 8, (64, 64, 64), 10)
ura = cv2.putText(ura, "2020", (450, 2000), cv2.FONT_HERSHEY_PLAIN, 13, (64, 64, 64), 20)

plt.figure(figsize=(10,15))
plt.imshow(ura)

?

スクリーンショット 2019-12-30 16.33.42.png

印刷

コマンドラインのlprコマンドから印刷を試みます。指定できるオプションはlpoptions -p [プリンタ名] -lでコマンドラインから確認できます。あとはご使用のプリンターの公式サイトを見れば、いい感じに印刷できるはず...?

import subprocess
from PIL import Image
from io import BytesIO

buf = BytesIO()

Image.fromarray(ura).save(buf, 'PNG')

# print out
p = subprocess.Popen('lpr -P Canon_TS5100_series -o media=Postcard -o InputSlot=rear -o MediaType=any'.split(), stdin=subprocess.PIPE)
p.communicate(buf.getvalue())

p.stdin.close()
buf.close()

おもて面も同様に指定すれば、いい感じに印刷できるはずです。

結果

↓裏面、いい感じ
IMG_4127.jpg

↓おもて面、二回印刷してフチなしとフチありを試したのですがいうまくいかず...
IMG_4128.jpg

印刷までこぎつけたのですが、サイズが合わない悔しい結果になってしまいました。アフィン変換したときに小さく切り出してしまい、誤差が生じている可能性や、プリンタ側で拡大縮小している可能性があります... (フチなしの時とフチありの時のちょうど中間に印刷できれば、ちょうどよくなるはず?)

まとめ

印刷がうまくできませんでした。どなたか知見をお持ちでしたら教えていただきたく思います。ExcelとWord万々歳。良いお年を!

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

FlaskとReact使ったTwitter認証をWebSocket使ってめっちゃ強引に行う

概要

reactにおける認証は下記のページなどで行っている人が複数人いる。
React Authentication with Twitter, Google, Facebook and Github
ReactでSPAを作り、Twitter認証(OAuth)でログインする。バックエンドはRails
ただ、自分は馬鹿なのでいまいちよくわからなかった(後、上記ページはサーバーサイドがexpressとかRailsでflaskは見つからなかった)。
そのため、小手先でめっちゃ強引な認証を書いた。

WebSocket使うのもReactのuseEffect使うのも、Flask使うのも初めてなので、参考程度にみてもらえると良い(後々、ちゃんとしたコードで書き直したい)

流れ

今回は「連携アプリ認証」のボタンを押すと連携が始まるようになっているので、そこの部分は適当に変えて欲しい。

  1. 認証を行うボタンをおす(js内handleClick関数)
  2. サーバーに"Twitter"の文字列が飛ぶ(js内handleClick関数)
  3. URLをサーバーが返す(Twitter認証用)(python内pipe関数)
  4. jsでURLで移動(js内ws.onmessage = function(e)内)
  5. Twitter認証画面
  6. 認証する
  7. クライアント側のページにredirect
  8. サーバーにauth_tokenとauth_verifierの情報がいく(useEffect内ws.onopenより)
  9. サーバーはtokenとverifierを使ってaccess_token_secretを作成する(python内user_timeline)
  10. Twitterから認証ユーザの最新のタイムライン一件を持ってくる(python内user_timeline)

認証などにはtweepyを用いている。

実際のコード

index.py
import os
import json
import sys
import tweepy
from flask import Flask, session, redirect, render_template, request, jsonify, abort, make_response
from flask_cors import CORS, cross_origin
from os.path import join, dirname
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler

app = Flask(__name__)
CORS(app)
#勝手に決める
app.secret_key = ""
#TwitterAppから持ってくる
CONSUMER_KEY = ""
CONSUMER_SECRET = ""

@app.route('/pipe')
def pipe():
   if request.environ.get('wsgi.websocket'):
       ws = request.environ['wsgi.websocket']
       while True:
            message = ws.receive()
            # print(message)
            #ボタンを押すとTwitterがwebsocketで送られるので送られたら発火
            if message == "Twitter":
                auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
                try:
                    # 連携アプリ認証用の URL を取得
                    redirect_url = auth.get_authorization_url()
                    session['request_token'] = auth.request_token
                except Exception as ee:
                    # except tweepy.TweepError:
                    sys.stderr.write("*** error *** twitter_auth ***\n")
                    sys.stderr.write(str(ee) + "\n")
                #websocketでurlを送り返す
                ws.send(redirect_url)
                ws.close()
                #return無いとエラーが出るため
                return redirect_url
            elif message != None:
                messages = json.loads(message)
                # print(messages)
                user_timeline(messages, ws)
def user_timeline(auths, ws):
    # tweepy でアプリのOAuth認証を行う
    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    verifier = str(auths["oauth_verifier"])
    # Access token, Access token secret を取得.
    auth.request_token['oauth_token'] = str(auths["oauth_token"])
    auth.request_token['oauth_token_secret'] = verifier
    try:
        access_token_secret = auth.get_access_token(verifier)
    except Exception as ee:
        print(ee)
        return ""

    print(access_token_secret)
    # tweepy で Twitter API にアクセス
    api = tweepy.API(auth)

    # user の timeline 内のツイートのリストを1件取得して返す
    for status in api.user_timeline(count=1):
        text = status.text
    # user の timeline 内のツイートのリストを1件取得して返す
    ws.send(text)
    ws.close()

def main():
    app.debug = True
    server = pywsgi.WSGIServer(("", 5000), app, handler_class=WebSocketHandler)
    server.serve_forever()

if __name__ == "__main__":
    main()
app.js
import React from 'react';
import './App.css';
import { useState, useEffect } from 'react';

function App() {
  const [flag, setFlag] = useState(false);
  const [userData, setUserData] = useState('');
  const [data, setData] = useState('');
  //webSocketとの通信
  const ws = new WebSocket('ws://localhost:5000/pipe');
  // レンダー前にwsがopenした後にurl内のverifierを返す
  useEffect(() => {
    ws.onopen = event => {
      if (userData == false && window.location.search.includes('verifier')) {
        setFlag(true);
        ws.send(getParam('oauth_verifier'));
      }
    };
    setUserData('true');
  });

  //url内の特定の要素を持ってくるためのコード
  function getParam(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, '\\$&');
    var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
      results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, ' '));
  }

  // サーバー側からメッセージが送られてきた際に受け取り、関数を発動する(Twitterの認証用URLに飛ぶため)
  ws.onmessage = e => {
    console.log(e);
    if (e.data.includes('oauth_token')) {
      window.location.href = e.data;
    } else {
      console.log(e);
      setData(e.data);
    }
  };
  //クリックした時(今回は文字をbuttonをクリックしたらサーバーにTwitterのもじが送られる)
  function handleClick() {
    console.log('rest');
    ws.send('Twitter');
  }
  console.log(flag);

  //レンダー要素の切替
  let render = flag ? (
    <div>{data}</div>
  ) : (
    <button onClick={handleClick}>連携アプリ認証</button>
  );
  return <>{render}</>;
}

export default App;

正直、めっちゃ適当なコードなので参考にできればする程度がちょうどいいと思う(後、websocketのエラーが出る。closedの状態で通信を行ってしまっているためだと思うので修正できたら編集します)

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

TensorFlow2 + Keras による画像分類に挑戦2 ~入力データを詳しくみてみる~

はじめに

手書き数字画像(MNIST)の分類を、Google Colaboratory 環境の TensorFlow2 + Keras でやってみよう(+Pythonや深層学習の理解も深めよう)という内容です。前回 は、TensorFlow の 公式HPのチュートリアル からサンプルコードを持ってきて、実際に実行してみる、というところまでやりました。

なお、MNIST(エムニスト)は、「図解速習DEEP LEARNING(著:増田知彰)」によれば、次のような由来があるデータだそうです。ここでは直接関係ありませんが、生のデータは、http://yann.lecun.com/exdb/mnist/ から入手できます。

NIST(National Institute of Standards and Technology database)の1つに、米国の国勢調査局職員と高校生が手書きした数字を持つデータセットがありました。それを機械学習でより使いやすく改変(Modified)したものが、"M"NISTです。

今回は、前回に示したサンプルコードのなかの トレーニング用データx_trainy_train)、テスト用データx_testy_test)について、その内容を詳しく見てみたり、matplotlib を使って可視化してみたりします。

それにあたり、まずは「多クラス分類問題」と「深層学習」について整理しておきます(トレーニング用データとテスト用データの位置づけを確認します)。

多クラス分類問題

手書き数字の認識は、多クラス分類問題というものに属します。多クラス分類問題とは、入力データに対して、そのカテゴリ(クラス)を予測するという問題です。カテゴリは、問題設定のなかで「犬」「猫」「鳥」のようにあらかじめ与えられており、入力データ(例えば画像)に対してそれが「犬」「猫」「鳥」のうち、どのカテゴリになるかを求める、といった問題になります。

多クラス分類.png

多クラス分類問題に対して様々なアプローチが提案されていますが、ここでは深層学習(ディープラーニング)を使って解決していきます。

深層学習

深層学習(ディープラーニング)は、教師付き機械学習という手法に属します。教師付き機械学習は、大きく「学習フェーズ」と「予測フェーズ(推論フェーズ、適用フェーズ)」という2段階から構成されます。

フェーズ.png

はじめに、学習フェーズでは、入力データ正解データ(=教師データ、正解データ、正解値、正解ラベル)をペアにしたものをモデルに大量に与えて、それらの関係を学習させます。これらの入力データと正解データのペア集合をトレーニング用データ(=学習用データ)と呼びます。そして、トレーニング用データを使って学習させたモデルを「学習済みモデル」といいます。

イメージ.png

つづく予測フェーズでは、学習済みモデルに対して、未知の入力データを与えて出力の予測(Predict)を行ないます。多クラス問題であれば、カテゴリ(例えば「犬」など)が予測出力となります。

そして、「学習済みモデルにどの程度の性能があるか」を測るのが評価(Evaluate)というプロセスになります。評価では、まず、トレーニングに使ったものとは異なる入力データと正解データを用意して、このうち入力データだけを学習済みモデルに与えて、予測データを得ます。そして、この得られた予測データについて、正解データを使って答え合わせ、採点をして評価値とします。具体的な評価指標としては、前回出てきた正答率(accuracy)、損失関数値(loss)のほかに、適合率や再現率など必要に応じて様々なものが採用されます。

MNISTのトレーニング用データ、テスト用データ

次のコードで、MNISTデータをダウンロードして、各変数(x_trainy_trainx_testy_test)に格納しています(プログラム全体は前回 を参照)。

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

ここで、*_train がトレーニング用(学習用)に割り当てられた入力&正解データ、*_test がテスト用(モデル評価用)に割り当てられた入力&正解データとなります。トレーニング用は 60,000件、テスト用は 10,000件 あります。

また、x_*** には入力データ(つまり手書き画像を表すデータ:28x28の256段階グレースケール)、y_*** には正解データ(「0」から「9」までのカテゴリ)が、配列的に格納されています。

まずは、実際に、それぞれが 60,000件、10,000件 のデータから構成されていることを len() で確認してみます。

# トレーニング用データ
print(len(x_train))  # 実行結果 -> 60000
print(len(y_train))  # 実行結果 -> 60000
# テスト用データ
print(len(x_test))   # 実行結果 -> 10000
print(len(y_test))   # 実行結果 -> 10000

次に、各データのタイプ(型)を確認してみます。

print(type(x_train)) # 実行結果 -> <class 'numpy.ndarray'>
print(type(y_train)) # 実行結果 -> <class 'numpy.ndarray'>
print(type(x_test))  # 実行結果 -> <class 'numpy.ndarray'>
print(type(y_test))  # 実行結果 -> <class 'numpy.ndarray'>

次に、y_train(=トレーニング用の正解データ)の内容を確認してみます。

print(y_train) # 実行結果 -> [5 0 4 ... 5 6 8]

0件目のデータの正解値は「5」、1件目のデータの正解値は「0」・・・、59,999件目のデータの正解値は「8」ということが分かりました。

次に、x_train(=トレーニング用の手書き画像を表すもの)の内容を確認してみます。全件を表示するととんでもないことになるので、先頭の x_train[0] のみを対象にします。

(x_train, y_train), (x_test, y_test) = mnist.load_data()
print(x_train[0].shape) # 実行結果 -> (28, 28)
print(x_train[0])       # 実行結果 -> 下記参照 

numpy.ndarray のデータは、.shape で大きさが確認できます。 (28, 28)、ということは、x_train[0]28行28列の2次元配列で構成されていることが分かります。また、print(x_train[0])の出力は次のようになります。

薄眼で眺めていただくと、ややいびつな手書きの「5」という数字が浮かんできます。これは、y_train[0] に格納されている「5」と一致しますね。

無題.png

各ピクセルデータは、0から255の範囲の値で構成されて、0が背景(白)で、255が最も濃い文字部(黒)になっていることが分かります。

60,000個の全てのデータについて、それを確認してみたいと思います。

import numpy as np
print(x_train.min())  # 最小値を抽出 # 実行結果 -> 0
print(x_train.max())  # 最大値を抽出 # 実行結果 -> 255

すべてのデータは0から255の範囲で構成されていることが確認できます。

ところで、60,000件のトレーニング用データのなかに、「0」から「9」までの各数字は何件ずつ存在するのでしょうか?基本的には、0から9までの10パターンがほぼ均等に存在していると思いますが、確認してみます。集計にpandasを利用します。

pandas版
import pandas as pd

tmp = pd.DataFrame({'label':y_train})
tmp = tmp.groupby(by='label').size()
display(tmp)
print(f'総数={tmp.sum()}')
実行結果
label
0    5923
1    6742
2    5958
3    6131
4    5842
5    5421
6    5918
7    6265
8    5851
9    5949
dtype: int64
総数=60000

「5」が少なくて「1」が多いといった多少のバラつきがあるようです。

なお、次のように pandas を使わなくても求めることができます。

numpy版
import numpy as np
tmp = list([np.count_nonzero(y_train==p) for p in range(10)])
print(tmp)                # 実行結果 -> [5923, 6742, 5958, 6131, 5842, 5421, 5918, 6265, 5851, 5949]
print(f'総数={sum(tmp)}') # 実行結果 -> 総数=60000

次回

  • matplotlib を使って入力データをグラフィカルに表示するところまで進めたかったのですが、記事が長くなってしまったので、それは次回にしたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudComposerでML Pipelineを構築してみた

本記事でやること

Google Cloud PlatformのCloud Composerを用いて以下のような機械学習を行う際の一連のタスクをオーケストレーションします。

  1. BigQueryから抽出したTrain/TestデータをCSVファイルにしてGCSへ置く
  2. ML EngineへTrainingのジョブを送る
  3. モデルのデプロイを行う
  4. ML EngineへPredictionのジョブを送る
  5. GCSに置かれているPrediction結果をBigQueryへLoadする

作成するAirflow上のノードとワークフローは下図の様になります。
image.png

対象読者

  • CloudComposer/AirFlowを触ったことのある方
  • ML Engine/DataFlowを触ったことのある方

使用言語とフレームワーク

  • Python 3.6.3
  • tensorflow 1.15

Airflowのバージョン

  • 1.10.6

GCPの各サービスのアーキテクチャ

上記AirflowのタスクをGCPのサービスでそれぞれを表現すると以下の様になります。

スクリーンショット 2019-12-30 14.36.56.png

1. Cloud Composerの環境設定を行う

以下のbashコマンドでは3つのことを行なっています。

1.Cloud Composerの環境構築

Cloud Composerの環境構築時に注意しないといけないこととしては、引数に--python-version 3を指定することです。デフォルトではpython2系が設定されています。

2.airflowへのライブラリーのインストール

冒頭で示したタスク一覧の中でSlackでメッセージをpostする箇所がありました。このタスクを実行する為にairflowにslackclientライブラリーをインストールする必要があります。
--update-pypi-packages-from-file引数にライブラリーの設定ファイルを指定します。

requirements.txt
slackclient~=1.3.2

3.airflow上の環境変数を設定

先述の通り、slackclientライブラリーを使ってslackへメッセージをpostする際にacccess_tokenが必要になるのでairflowの環境変数にaccess_tokenを設定しおくと便利なので予め設定しておきます。(dagファイルにaccess_tokenをベタ書きするのはあまりよろしくない)

#!/usr/bin/env bash

ENVIRONMENT_NAME=dev-composer
LOCATION=us-central1

# 変数を読み込む
eval `cat airflow/config/.secrets.conf`
echo ${slack_access_token}

# cloud composerの環境作成
gcloud composer environments create ${ENVIRONMENT_NAME} \
    --location ${LOCATION} \
    --python-version 3

# airflowの環境にライブラリーをインストール
gcloud composer environments update ${ENVIRONMENT_NAME} \
--update-pypi-packages-from-file airflow/config/requirements.txt \
--location ${LOCATION}

# airflow上の環境変数を設定
gcloud composer environments run \
  --location=${LOCATION} \
  ${ENVIRONMENT_NAME} \
  variables -- \
  --set slack_access_token ${slack_access_token} project_id ${project_id}

dagファイルの実装

今回作成したdagファイルは以下の通りになっています。
これだけだと説明が不十分なので、タスク毎にコードを切り分けて説明をしていきます。

import os
import airflow
import datetime
from airflow.models import Variable
from airflow.operators.bash_operator import BashOperator
from airflow.operators.dummy_operator import DummyOperator
from airflow.contrib.operators.dataflow_operator import DataFlowPythonOperator
from airflow.operators.slack_operator import SlackAPIPostOperator
from airflow import configuration
from airflow.utils.trigger_rule import TriggerRule
from airflow.contrib.operators.mlengine_operator \
    import MLEngineTrainingOperator, MLEngineBatchPredictionOperator

BUCKET = 'gs://your_bucket'
PROJECT_ID = Variable.get('project_id')
REGION = 'us-central1'
YESTERDAY = datetime.datetime.now() - datetime.timedelta(days=1)
PACKAGE_URI = BUCKET + '/code/trainer-0.1.tar.gz'
OUTDIR = BUCKET + '/trained_model'
DATAFLOW_TRAIN_FILE = os.path.join(
    configuration.get('core', 'dags_folder'), 
    'dataflow', 'extract_train_data.py')

DATAFLOW_PRED_FILE = os.path.join(
    configuration.get('core', 'dags_folder'),
    'dataflow', 'extract_pred_data.py')

DATAFLOW_LOAD_FILE = os.path.join(
    configuration.get('core', 'dags_folder'),
    'dataflow', 'load.py')

DEFAULT_ARGS = {
    'owner': 'Composer Example',
    'depends_on_past': False,
    'email': [''],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': datetime.timedelta(minutes=5),
    'start_date': YESTERDAY,
    'project_id': Variable.get('project_id'),
    'dataflow_default_options': {
        'project': Variable.get('project_id'),
        'temp_location': 'gs://your_composer_bucket/temp',
        'runner': 'DataflowRunner'
    }
}

def get_date():
    jst_now = datetime.datetime.now()
    dt = datetime.datetime.strftime(jst_now, "%Y-%m-%d")
    return dt


with airflow.DAG(
        'asl_ml_pipeline',
        'catchup=False',
        default_args=DEFAULT_ARGS,
        schedule_interval=datetime.timedelta(days=1)) as dag:

    start = DummyOperator(task_id='start')

    ####
    # ML Engineでのtrainingを行うタスク
    ####

    job_id = 'dev-train-{}'.\
        format(datetime.datetime.now().strftime('%Y%m%d%H%M'))
    job_dir = BUCKET + '/jobs/' + job_id

    submit_train_job = MLEngineTrainingOperator(
        task_id='train-model',
        project_id=PROJECT_ID,
        job_id=job_id,
        package_uris=[PACKAGE_URI],
        region=REGION,
        training_python_module='trainer.task',
        training_args=[f'--output_dir={OUTDIR}',
                       f'--job_dir={job_dir}',
                       '--dropout_rate=0.5',
                       '--batch_size=128',
                       '--train_step=1'
                       ],
        scale_tier='BASIC_GPU',
        python_version='3.5'
    )
    ####
    # modelをdeployするタスク
    #### 

    BASE_VERSION_NAME = 'v1_0'
    VERSION_NAME = '{0}_{1}'.\
        format(BASE_VERSION_NAME, datetime.datetime.now().strftime('%Y_%m_%d'))
    MODEL_NAME = 'dev_train'

    deploy_model = BashOperator(
        task_id='deploy-model',
        bash_command='gcloud ml-engine versions create '
                     '{{ params.version_name}} '
                     '--model {{ params.model_name }} '
                     '--origin $(gsutil ls gs://your_bucket/trained_model/export/exporter | tail -1) '
                     '--python-version="3.5" '
                     '--runtime-version=1.14 ',
        params={'version_name': VERSION_NAME,
                'model_name': MODEL_NAME}
    )

    ####
    # ML Engineでバッチ予測を行うタスク
    ####
    today = get_date()

    input_path = BUCKET + f'/preprocess/{today}/prediction/prediction-*'
    output_path = BUCKET + f'/result/{today}/'

    batch_prediction = MLEngineBatchPredictionOperator(
        task_id='batch-prediction',
        data_format='TEXT',
        region=REGION,
        job_id=job_id,
        input_paths=input_path,
        output_path=output_path,
        model_name=MODEL_NAME,
        version_name=VERSION_NAME
    )

    ####
    # DataFlowでデータ抽出を行うタスク
    ####
    job_args = {
        'output': 'gs://your_bucket/preprocess'
    }

    create_train_data = DataFlowPythonOperator(
        task_id='create-train-data',
        py_file=DATAFLOW_TRAIN_FILE,
        options=job_args
    )

    create_pred_data = DataFlowPythonOperator(
        task_id='create-pred-data',
        py_file=DATAFLOW_PRED_FILE,
        options=job_args
    )
    ####
    # DataFlowでBigQueryへデータをloadするタスク
    ####
    load_results = DataFlowPythonOperator(
        task_id='load_pred_results',
        py_file=DATAFLOW_LOAD_FILE
    )
        post_success_slack_train = SlackAPIPostOperator(
        task_id='post-success-train-to-slack',
        token=Variable.get('slack_access_token'),
        text='Train is succeeded',
        channel='#feed'
    )

    post_fail_slack_train = SlackAPIPostOperator(
        task_id='post-fail-train-to-slack',
        token=Variable.get('slack_access_token'),
        trigger_rule=TriggerRule.ONE_FAILED,
        text='Train is failed',
        channel='#feed'
    )
    ####
    # SlackへメッセージをPOSTするタスク
    ####
    post_success_slack_pred = SlackAPIPostOperator(
        task_id='post-success-pred-to-slack',
        token=Variable.get('slack_access_token'),
        text='Prediction is succeeded',
        channel='#feed'
    )

    post_fail_slack_pred = SlackAPIPostOperator(
        task_id='post-fail-pred-to-slack',
        token=Variable.get('slack_access_token'),
        trigger_rule=TriggerRule.ONE_FAILED,
        text='Prediction is failed',
        channel='#feed'
    )


    end = DummyOperator(task_id='end')

    start >> [create_train_data, create_pred_data] >> submit_train_job \
        >> [post_fail_slack_train, post_success_slack_train]
    post_fail_slack_train >> end

    post_success_slack_train >> deploy_model >> batch_prediction \
        >> load_results \
        >> [post_success_slack_pred, post_fail_slack_pred] >> end

BigQueryからのデータ抽出及びGCSへの書き出し

Training Phaseで行なっている最初のタスクです。(以下図の赤枠)
BigQueryからDataFlowを使ってデータの抽出及びGCSの適当なバケットへデータを置くという処理を行なっています。

スクリーンショット 2019-12-29 16.43.40.png

Dagファイルの説明

  • 定数 DATAFLOW_TRAIN_FILE / DATAFLOW_PRED_FILE

    • DataFlowの実行ファイルが置かれているファイルパス
    • Cloud Composerの環境構築時に作成されたBucket内のdagsディレクトリ以下のファイルは数秒間隔でairflowのworkerによって同期されています。
  • 定数DEFAULT_ARGS

    • dataflow_default_optionsの引数にDataFlow実行時の環境変数を設定します。
  • DataFlowPythonOperatorクラス

    • DataFlowをpythonファイル使用してJOBを実行する際のクラス
    • py_file引数に実行ファイルが置かれているパスをしているする
    • options引数に実行ファイルに渡す引数を指定する
      • 今回はGCSにデータを置く際のファイルパスを指定しています
DATAFLOW_TRAIN_FILE = os.path.join(
    configuration.get('core', 'dags_folder'), 
    'dataflow', 'extract_train_data.py')

DATAFLOW_PRED_FILE = os.path.join(
    configuration.get('core', 'dags_folder'),
    'dataflow', 'extract_pred_data.py')

DEFAULT_ARGS = {
    'owner': 'Composer Example',
    'depends_on_past': False,
    'email': [''],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': datetime.timedelta(minutes=5),
    'start_date': YESTERDAY,
    'project_id': Variable.get('project_id'),
    'dataflow_default_options': {
        'project': Variable.get('project_id'),
        'temp_location': 'gs://your_composer_bucket/temp',
        'runner': 'DataflowRunner'
    }
}
    ####
    # DataFlowでデータ抽出を行うタスク
    ####
    # GCSにデータを置く際のファイルパス
    job_args = {
        'output': 'gs://your_bucket/preprocess'
    }

    create_train_data = DataFlowPythonOperator(
        task_id='create-train-data',
        py_file=DATAFLOW_TRAIN_FILE,
        options=job_args
    )

    create_pred_data = DataFlowPythonOperator(
        task_id='create-pred-data',
        py_file=DATAFLOW_PRED_FILE,
        options=job_args
    )

DataFlow実行ファイルの説明

以下のファイルは、Trainingを行うための、train datatest dataに分割しGCSへ置く処理となっています。
(また、記事の簡略化のためtrain datatest dataに分割するためのクエリの解説は割愛させて頂きます。timestampのカラムがあることを前提にハッシュ化を行い、除算した余りの値で分割しております)

ここで気をつけたいポイントは2点です。

  • CSVファイルへの変換

    • 最終的にCSVファイルとして出力したいのでBigQueryから抽出したデータをカンマ区切りの形式に変換する必要があります。
    • ここでは、to_csvという関数を用意しております。
  • DataFlowのPythonバージョン

    • DataFlowのPythonは3.5.xなので3.6から追加されたf-stringsなどの文法は使えません。
import os
import argparse
import logging
from datetime import datetime

import apache_beam as beam
from apache_beam.options.pipeline_options import \
    PipelineOptions

PROJECT = 'your_project_id'


def create_query(phase):
    base_query = """
    SELECT
        *,
        MOD(ABS(FARM_FINGERPRINT(CAST(timestamp AS STRING))), 10) AS hash_value
    FROM
        `dataset.your_table`
    """

    if phase == 'TRAIN':
        subsumple = """
        hash_value < 7
        """
    elif phase == 'TEST':
        subsumple = """
        hash_value >= 7
        """

    query = """
    SELECT 
        column1,
        column2,
        column3,
        row_number()over() as key
    FROM 
        ({0})
    WHERE {1}
    """.\
        format(base_query, subsumple)

    return query


def to_csv(line):
    csv_columns = 'column1,column2,column3,key'.split(',')
    rowstring = ','.join([str(line[k]) for k in csv_columns])
    return rowstring


def get_date():
    jst_now = datetime.now()
    dt = datetime.strftime(jst_now, "%Y-%m-%d")

    return dt


def run(argv=None):
    parser = argparse.ArgumentParser()

    parser.add_argument(
        '--output',
        required=True
    )

    known_args, pipeline_args = \
        parser.parse_known_args(argv)

    options = PipelineOptions(pipeline_args)
    with beam.Pipeline(options=options) as p:
        for phase in ['TRAIN', 'TEST']:
            query = create_query(phase)

            date = get_date()
            output_path = os.path.join(known_args.output, date,
                                       'train', "{}".format(phase))

            read = p | 'ExtractFromBigQuery_{}'.format(phase) >> beam.io.Read(
                beam.io.BigQuerySource(
                    project=PROJECT,
                    query=query,
                    use_standard_sql=True
                )
            )

            convert = read | 'ConvertToCSV_{}'.format(phase) >> beam.Map(to_csv)

            convert | 'WriteToGCS_{}'.format(phase) >> beam.io.Write(
                beam.io.WriteToText(output_path, file_name_suffix='.csv'))


if __name__ == '__main__':
    logging.getLogger().setLevel(logging.INFO)
    run()

ML Engineでのtraining及びdeploy

ここでは、ML Engineを使ったtraining及び学習したモデルのdeployを行うタスクの説明を行いまいす。
また、今回はML Engineで使用する実行ファイルtask.pyやモデルファイルmodel.pyについての説明は割愛させて頂きます。

スクリーンショット 2019-12-29 16.43.40.png

今回ML Engine用にバケットを用意しております。
なので、Cloud Composerの環境構築時で作成されるバケットと合わせると合計2つバケットを使い分けているので注意してください。

.
├── your_bucket 
│    ├── code // 学習に必要なmodel.pyやtask.py等を固めたgzファイルを置く
│    │
│    └── trainde_model  // 学習済みのmodelファイルが置かれる
│
└── your_composer_bucket // cloud composerの環境構築時に作成されるバケット

Dagファイルの説明

  • 定数PACKAGE_URI

    • ML Engineの実行ファイルが置いてあるファイルパス
    • 今回はtrainer.pymodel.pyなどを固めたgzファイルを上記のgs://your_bucket/code以下に置いています。
    • MLEngineTrainingOperatorクラスのpackage_uris引数にこの定数を指定しています。
  • BashOperatorクラス

    • bashコマンドを実行する際のクラス
    • 今回は学習済みのモデルファイルをdeployする際にbashコマンド使用しています。
      • gcloud ml-engine versions createを実行することでmodelのdeployを行なっています
    • (おそらく)MLEngineVersionOperatorクラスでも同様のことが行えると思いますが、今回はbashコマンドを利用しました。
PACKAGE_URI = BUCKET + '/code/trainer-0.1.tar.gz'
OUTDIR = BUCKET + '/trained_model'

    job_id = 'dev-train-{}'.\
        format(datetime.datetime.now().strftime('%Y%m%d%H%M'))
    job_dir = BUCKET + '/jobs/' + job_id

    submit_train_job = MLEngineTrainingOperator(
        task_id='train-model',
        project_id=PROJECT_ID,
        job_id=job_id,
        package_uris=[PACKAGE_URI],
        region=REGION,
        training_python_module='trainer.task',
        training_args=[f'--output_dir={OUTDIR}',
                       f'--job_dir={job_dir}',
                       '--dropout_rate=0.5',
                       '--batch_size=128',
                       '--train_step=1'
                       ],
        scale_tier='BASIC_GPU',
        python_version='3.5'
    )
    today = get_date()

    BASE_VERSION_NAME = 'v1_0'
    VERSION_NAME = '{0}_{1}'.\
        format(BASE_VERSION_NAME, datetime.datetime.now().strftime('%Y_%m_%d'))
    MODEL_NAME = 'dev_model'

    deploy_model = BashOperator(
        task_id='deploy-model',
        bash_command='gcloud ml-engine versions create '
                     '{{ params.version_name}} '
                     '--model {{ params.model_name }} '
                     '--origin $(gsutil ls gs://your_bucket/trained_model/export/exporter | tail -1) '
                     '--python-version="3.5" '
                     '--runtime-version=1.14 ',
        params={'version_name': VERSION_NAME,
                'model_name': MODEL_NAME}
    )

ML Engineでのバッチ予測

ここでは、先でdeployしたモデルを使ってバッチ予測を行うタスクについて説明をします。

スクリーンショット 2019-12-29 16.43.40.png

  • 定数 input_path

    • 先で行なった予測用に抽出しGCSにおいたCSVファイルのパス
  • 定数 output_path

    • 予測した結果を置くGCSのファイルパス

Dagファイルの説明

    input_path = BUCKET + f'/preprocess/{today}/prediction/prediction-*'
    output_path = BUCKET + f'/result/{today}/'

    batch_prediction = MLEngineBatchPredictionOperator(
        task_id='batch-prediction',
        data_format='TEXT',
        region=REGION,
        job_id=job_id,
        input_paths=input_path,
        output_path=output_path,
        model_name=MODEL_NAME,
        version_name=VERSION_NAME
    )

バッチ予測結果のBigQueryへのLoad

ここでは、先で予測した予測結果をDataFlowを使ってBigQueryにLoadするタスクについて説明します。

スクリーンショット 2019-12-30 14.36.56.png

Dagファイルの説明

Dagファイルについて、冒頭で説明した"BigQueryからデータを抽出する"タスクと同様になります。
定数DATAFLOW_LOAD_FILEにDataFlowの実行ファイルがおかれているGCSのファイルパスを指定しています。

DATAFLOW_LOAD_FILE = os.path.join(
    configuration.get('core', 'dags_folder'),
    'dataflow', 'load.py')

    load_results = DataFlowPythonOperator(
        task_id='load_pred_results',
        py_file=DATAFLOW_LOAD_FILE
    )

DataFlow実行ファイルの説明

以下のファイルでは、GCSに置かれているファイル読み込み、Json形式へと変換しBigQueryの適当なテーブルへloadをしております。
ここで気をつけたいこととしては

  • バッチ予測結果はjson形式のテキストファイルとしてGCSに置かれます。
    • テキストファイルに書かれているpredictionの出力結果の値などはリスト形式として記載されているのでそのままloadしようとすると型が合わなくてエラーが生じます。
      • {"key": [0], "prediction: [3.45...]"}
import logging
import argparse

import apache_beam as beam
from apache_beam.options.pipeline_options import PipelineOptions

BUCKET_NAME = 'your_bucket'
INPUT = 'gs://{}/result/prediction.results-*'.format(BUCKET_NAME)


def convert(line):
    import json
    record = json.loads(line)
    return {'key': record['key'][0], 'predictions': record['predictions'][0]}


def run(argv=None):
    parser = argparse.ArgumentParser()

    known_args, pipeline_args = \
        parser.parse_known_args(argv)

    options = PipelineOptions(pipeline_args)

    with beam.Pipeline(options=options) as p:
        dataset = 'your_dataset.results'

        read = p | 'ReadPredictionResult' >> beam.io.ReadFromText(INPUT)
        json = read | 'ConvertJson' >> beam.Map(convert)
        json | 'WriteToBigQuery' >> beam.io.Write(beam.io.BigQuerySink(
            dataset,
            schema='key:INTEGER, predictions:FLOAT',
            create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
            write_disposition=beam.io.BigQueryDisposition.WRITE_TRUNCATE))


if __name__ == '__main__':
    logging.getLogger().setLevel(logging.INFO)
    run()


おわりに

普段はAWSを使っていますが、GCPのサービスについて触れる機会があったのでML関係のサービスについてまとめてみました。MLのワークフローについて悩まれている方の参考になれば幸いです。

MLEngineでtrainingしたmodelをそのままdeployする箇所がありますが、あまりオススメできません。なにがしか学習したモデルの精度を測る/比較する機構を作り、そのタスクを挟んでからdeployすることをオススメします。

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

Python学習ノート_006

第4章のサンプルコードは下記のようです。

  • ポイント
    • リストの長さはlen()で計算して、forループにインデックスの上限値として使える
    • printにコンマで区切った複数の値の間に1スペースがあるため、それを回避する方法は文字列の連結で1つ文字列として出力する
sample04.py
scores = [60, 50, 60, 58, 54, 54,
          58, 50, 52, 54, 48, 69,
          34, 55, 51, 52, 44, 51,
          69, 64, 66, 55, 52, 61,
          46, 31, 57, 52, 44, 18,
          41, 53, 55, 61, 51, 44]

length = len(scores)

for i in range(length):
    print("テストの案 #" + str(i),"のscore:",scores[i])

high_score = max(scores)
max_list = []
for i in range(length):
    if scores[i] == high_score:
        max_list.append(i)

print("テストの総数:",length)
print("最高の点数:",max(scores))
print("最大値のインデックス:",max_list)
  • 出力結果
    サンプル4の出力結果の一部
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

宿題がめんどくさかったのでPythonで管理会計をする

私は大学では経営学を学んでいるのでもちろん会計的な授業もあります。その中の一つである管理会計の授業の問題がめんどくさいのでプログラミングでできないかなと考えました。テストの時には使えないので結局計算式は暗記しないといけないのですが、まぁ今回はいいでしょう。

管理会計とは?

管理会計(かんりかいけい、英語:management accounting)は、企業会計の一種。主として、会計情報を経営管理者の意思決定や組織内部の業績測定・業績評価に役立てることを目的としている。対義語は財務会計である。企業外部の利害関係者に対する情報提供を目的とする財務会計とは、大きく性格が異なっている。

引用 wikipedia

簡単にいうと一般的な簿記試験などである商業簿記などの会計は企業の外部の人にむけて作られているものなんです。なので規則に乗っ取って作らないといけません。しかし、管理会計の目的は企業の意思決定に役に立つ様な会計的な情報を測定し、レポートを作成することなので目的が全く違うのです。

投資可能(受託可能)な案件かどうか判定する

nowは現在の生産量
capacity_percentは現在の操業度
trade_request_amountは相手の求めている生産量

def Investable(now, capacity_percent, trade_request_amount):
    max_amount= now / capacity_percent
    now_can_use = max_amount- now
    if now_can_use  >trade_request_amount:
        print('Investable')
    else:
        print('No')

Investable(42500, 85, 7000 )
#実行結果
No

実行の結果この投資案は実行できないということが分かりました。

差額原価収益分析

差額原価収益分析とは、安値受注のような特殊な非反復的意思決定を行うときに、追加的に発生する収益及び原価・費用のみを抽出して差額利益を計算し、迅速な意思決定を行う手法である。

productive_volume  生産個数
revenue_per_item  製品1個あたりの利益
produce_cost_per_item 製品1個あたりの生産コスト  
sales_cost   販売費
fixed_cost  固定費

def Differential_cost_revenue(productive_volume, revenue_per_item,
produce_cost_per_item, sales_cost, fixed_cost):
    a = revenue_per_item * productive_volume 
    b = produce_cost_per_item * productive_volume
    c = sales_cost * productive_volume
    cost = b+c +fixed_cost
    if  a- cost >0:
        print('think voting')
        return(a-cost)
    else:
        print('stop voting')



Differential_cost_revenue(6500, 450, 230, 30, 435000)
#実行結果
think voting
800000    

まとめ

今回は大学の授業の管理会計の宿題を簡単にやってくれるプログラムを作って見ました。このコードを書いたときは手元に問題のプリントがあったのですが、今はないので細かい問題の条件を忘れてしまいましたがコードが残っていたので想像でコードを書いて見ました笑

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

[2−1]作成したQRコードを読み取り識別する

この記事は"QRコードを使った入場システムを自作する"シリーズの記事です。

前回

"[1]独自のフォーマットに基づいてQRコードを発行する"では独自のフォーマットのQRコードを発行してデータベースに登録することができました。
今回はデータベースに登録したQRコードの読み取り方です。

QRコードを読み取るには

スキャナーを用意する予算がなかったのiOSアプリのPythonista3を利用してQRコードを読み取ります。

手順

Pythonista3を利用しなくてもなんでも読み取った後の処理は同じです。

  • QRコードをスキャン
  • 読み取ったコードを文字列に変換
  • MySQLで検索をかける

Pythonista3でQRコードを読み取る方法

今回一番きつかったのがこの部分で、objective-cをPython上で動かさなければいけませんでした。
Pythonistaには標準でobjc_utilというモジュールが入っているのでそれを利用します。

とは言っても...

objective-cなんか生まれてから一度も利用したことがなくswiftが主流?になってきているので今回はこちらを流用させていただいた。(一部改変)

ということで

以下がソースコードになる。

QRscanner1.py
# Barcode scanner demo for Pythonista

from objc_util import *
from ctypes import c_void_p
import mysql.connector as testdb
import ui
import sound


found_codes = set()
main_view = None

AVCaptureSession = ObjCClass('AVCaptureSession')
AVCaptureDevice = ObjCClass('AVCaptureDevice')
AVCaptureDeviceInput = ObjCClass('AVCaptureDeviceInput')
AVCaptureMetadataOutput = ObjCClass('AVCaptureMetadataOutput')
AVCaptureVideoPreviewLayer = ObjCClass('AVCaptureVideoPreviewLayer')
dispatch_get_current_queue = c.dispatch_get_current_queue
dispatch_get_current_queue.restype = c_void_p

#QRコードの読み取り
def captureOutput_didOutputMetadataObjects_fromConnection_(_self, _cmd, _output, _metadata_objects, _conn):
    objects = ObjCInstance(_metadata_objects)
    for obj in objects:
        s = str(obj.stringValue())
        if s not in found_codes:
            found_codes.add(s)
    #読み取り終了後にWindowを閉じる。
    main_view.close()

MetadataDelegate = create_objc_class('MetadataDelegate', methods=[captureOutput_didOutputMetadataObjects_fromConnection_], protocols=['AVCaptureMetadataOutputObjectsDelegate'])

@on_main_thread
def main():
    global main_view
    delegate = MetadataDelegate.new()
    main_view = ui.View(frame=(0, 0, 400, 400))
    main_view.name = 'Barcode Scanner'
    session = AVCaptureSession.alloc().init()
    device = AVCaptureDevice.defaultDeviceWithMediaType_('vide')
    _input = AVCaptureDeviceInput.deviceInputWithDevice_error_(device, None)
    if _input:
        session.addInput_(_input)
    else:
        print('Failed to create input')
        return
    output = AVCaptureMetadataOutput.alloc().init()
    queue = ObjCInstance(dispatch_get_current_queue())
    output.setMetadataObjectsDelegate_queue_(delegate, queue)
    session.addOutput_(output)
    output.setMetadataObjectTypes_(output.availableMetadataObjectTypes())
    prev_layer = AVCaptureVideoPreviewLayer.layerWithSession_(session)
    prev_layer.frame = ObjCInstance(main_view).bounds()
    prev_layer.setVideoGravity_('AVLayerVideoGravityResizeAspectFill')
    ObjCInstance(main_view).layer().addSublayer_(prev_layer)
    session.startRunning()
    main_view.present('sheet')
    main_view.wait_modal()
    session.stopRunning()
    delegate.release()
    session.release()
    output.release()
    #QRコードを読み取った際の処理
    if found_codes:
        #cur = conn.cursor()

        scan_code = str(''.join(found_codes))

        #table: QRidのデータを照合
        table = 'QRid'
        cur.execute("select * from QRid where QR=%s",(scan_code,))

        result = cur.fetchall()

        if len(result) == 0:
            sound.play_effect('digital:PhaserUp4')
            print('該当するデータがありません。')
            vibrate([50,0,50,0,50,0,50])

        else:
            sound.play_effect('arcade:Coin_5')
            print('ID'+'{:>15}'.format('DATE')+'{:>11}'.format('TIME'))
            for i in result:
                print(*i)


if __name__ == '__main__':
    #MySQLへ接続
    conn = testdb.connect(
        user = 'test', 
        password = '12345', 
        host = '192.168.x.x',
        port = '3306',
        database='QRtest'
    )

    #接続ステータスを確認
    conn.ping(reconnect=True)
    print('[Status] ', end='')
    print(conn.is_connected())

    cur = conn.cursor()

    main()

    #MySQLへの接続を終了
    cur.close()
    conn.commit()
    conn.close()

スクリプトを走らせるとカメラウィンドウが表示されます。
IMG_4029.PNG
正常に読み取られるとこのウィンドウが閉じてコンソールが開きます。
IMG_4028.PNG
MySQL上に登録されている情報が表示されれば成功です。
スクリーンショット 2019-12-30 15.05.38.png

次回予告

[2-2]作成したQRコードを読み取り識別する。でよりわかりやすいGUIを追加します。読み取り機は次回で完成です。
IMG_3969.PNG

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

【忘備録】Minimum Covariance Determinantを使用したマーケットの異常探知

Minimum Covariance Determinant or MLEで求めた共分散行列による異常探知比較

飛んだ値に思いっ切り引きずられるのを避けるためにも、マーケットデータと相性良いメソッドかと

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
from sklearn.covariance import EmpiricalCovariance, MinCovDet
from sklearn.covariance import EmpiricalCovariance

warnings.filterwarnings('ignore')

plt.style.use('seaborn-darkgrid')
plt.rcParams['axes.xmargin'] = 0.01
plt.rcParams['axes.ymargin'] = 0.01

ReadDFに週次の各種マーケットリターン(為替、株、債券etc)

# ルックバック過去50週間リターンを用いて算出

ts_out = pd.DataFrame()
for date in ReadDF.dropna(axis=0)[50:].index:
    x = ReadDF[:date][-50:]
    x = (x/x.std()).dropna(axis=0)

    mcd.fit(x[:-1])
    anomaly_score_mcd = mcd.mahalanobis(x[-1:])

    mle.fit(x[:-1])
    anomaly_score_mle = mle.mahalanobis(x[-1:])

    out = pd.DataFrame([anomaly_score_mcd, anomaly_score_mle]).T

    out.columns = ['mcd', 'mle']
    out.index = [date]

    ts_out = pd.concat([ts_out, out], axis=0)
fig = plt.figure(figsize=(15, 10), dpi=80)
out = pd.DataFrame(ts_out/ts_out.std())  # 規格化
sns.set_palette("hls", len(out.columns))
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(out, alpha=0.6)
plt.legend(out.columns)
plt.show()

Anomaly.png

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

【AWS IoT】モノにアタッチされていない証明書を削除する

目的

AWS IoTにおいて、モノにアタッチされていない証明書を削除する。
不要な証明書を大量に作成してしまった場合用。

クラス

import boto3

class CertKiller():

    def __init__(self):
        # AWS IoTを操作するクラスをインスタンス化
        self.client = boto3.client('iot')

        return


    def delete_not_attached_cert_all(self):
        '''
        モノにアタッチされていない証明書を削除
        '''
        # 証明書情報のリストを取得
        list_cert = self.get_list_cert()

        # モノにアタッチされていない証明書を削除
        for cert in list_cert:
            self.__delete_not_attached_cert(cert)

        return


    def get_list_cert(self):
        '''
        証明書情報のリストを取得
        '''
        list_cert = self.client.list_certificates(pageSize=100)['certificates']

        return list_cert 


    def __delete_not_attached_cert(self, cert):
        '''
        証明書がどのモノにもアタッチされていなかった場合、削除
        '''
        # 証明書情報を取得
        cert_arn = cert['certificateArn']
        cert_id = cert['certificateId']

        # 証明書をアタッチしてあるモノの一覧を取得
        thing_attached_cert = self.client.list_principal_things(principal=cert_arn)['things']
        print(cert_arn, thing_attached_cert)

        # 証明書がどのモノにもアタッチされていなかった場合、削除
        if len(thing_attached_cert) == 0:
            self.__delete_cert(cert_arn, cert_id)
        else:
            pass

        return


    def __delete_cert(self, cert_arn, cert_id):
        '''
        証明書を削除
        '''    
        # 削除前に無効化する必要がある
        self.client.update_certificate(certificateId=cert_id, newStatus='INACTIVE')

        # 削除前にポリシーをデタッチする必要がある
        self.__detach_all_policy(cert_arn, cert_id)

        # 削除
        self.client.delete_certificate(certificateId=cert_id, forceDelete=False)
        print('{} has been deleted.'.format(cert_arn))

        return


    def __detach_all_policy(self, cert_arn, cert_id):
        '''
        証明書にアタッチされている全てのポリシーをデタッチ
        '''    
        # 証明書にアタッチされているポリシーのリストを取得
        list_policy = self.client.list_attached_policies(target=cert_arn)['policies']

        # デタッチ
        for policy in list_policy:
            policy_name = policy['policyName']
            self.client.detach_policy(policyName=policy_name, target=cert_arn)

        return

実行

cert_killer = CertKiller()
cert_killer.delete_not_attached_cert_all()

備考

  • もし使用いただける場合は自己責任でお願いします。
  • デバイスにアタッチされている証明書が削除される心配はありません。
    delete_certificate() で forceDelete=Falseとしているため)
  • モノにアタッチされていない=不要 とは限らないと思いますので、
    複数名でAWS IoTを使用している場合は、削除前に他のユーザーに確認を取ると良いと思います。

感想

めちゃめちゃ初心者なので、本当に些細なことでもご指摘・コメント等いただけますと幸いです。

参照

Boto 3 Documentation

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

selenium/pythonだってAzure DevOps pipelineでヘッドレス実行したいよね?そうだよね?

本投稿はAzure DevOps Advent Calendar 2019の22日目の投稿になります。

DevOpsを推進するうえで重要な要素となる自動化ですが、様々な方法が考えられますし、組織に応じて導入を進めていく必要があります。組織のカルチャーを無視してコンセンサスが得られない状況で独自のツールが乱立してそれが運用に与える効果を測ることができればいいなと考えているsasukehです。皆様いかがお過ごしでしょうか?

巷では、seleniumを使って自動化という記事がかなりの数上がってくるようになりました。その効果もあり、WebのE2Eテストをseleniumで自動化するというかたも増えてきているように思えます。Test Driven Developmentもめちゃくちゃ重要でソフトウェアを検証、計測することで、その開発が正しい方向を向いているかの指標となります。なので、TDD始めましょう!というのは簡単だけど、じゃあどうすればいいの?という目的はわかったけど、手段がわからないという状況にいる方もいるのでは?と思います。とくにAzure DevOpsを使ってSeleniumでUIのテスト自動化したいけど、やっぱりagentにGUIが必要で、CLIは向いてないよね?みたいな解釈をしているかたがいらっしゃいますが、実は、seleniumには、headless実行と呼ばれる、GUIなしで実行する方法があります。また、Screenshotも取れるので、ZipでまとめてBlobでアップロードする方法もあります。

今年の初め頃に社内のチームメンバーとハッカソンしたときのコードなりtipsがあったのですが、なかなか公開できていなかったので、今日これをまとめようと思います。

つづく

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

【python】argparseを使ってコマンドラインで動く簡単なプログラムを作ってみた

ArgumentParser (argparse)

Pythonで書いたプログラムをコマンドラインで実行する時、ArgumentParser (argparse)を用いれば簡単にコマンドライン引数を使うことができます。

コマンドライン引数とは

コマンドライン引数とは次のようなもののことを言います。

python test.py -t 10

使ってみる

瞬時式について

瞬時式は一般に

e=V_{m} \sin(2 \pi f t \pm \phi)

で表されます。今回は

V_{m}[V],f[Hz],\phi[deg]

をコマンドライン変数で変更([]内はプログラム中での単位)し、様々なグラフの描画を試みます。

ソース

ライブラリの読み込み。argparseを読めば使えます。

import argparse

それぞれの引数の初期値などを定義

    parser = argparse.ArgumentParser()
    parser.add_argument('--phase', '-p', type=float, default=0, help='Definition of a point in time')
    parser.add_argument('--voltage', '-v', type=float, default=100, help='Definition of a effective value')
    parser.add_argument('--frequency', '-f', type=float, default=60, help='Definition of a frequency')
    parser.add_argument('--sampletime', '-t', type=float, default=0.0001, help='Definition of sampletime')    
args = parser.parse_args()

指定した数値の表示

    print('phase : {}\n'.format(args.phase))
    print('voltage : {}\n'.format(args.voltage))
    print('frequency : {}\n'.format(args.frequency))

グラフの描画

    t = np.arange(0, 1/60, args.sampletime)
    y1 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t + np.deg2rad(args.phase))
    y2 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t)
    plt.plot(t, y1)
    plt.plot(t, y2)

    plt.grid(True)

    plt.show()

    #plt.savefig("img.png")

コマンドラインで

python test.py -p 45

とすれば

Figure_1.png

を得ます。

コード全体

import numpy as np
import argparse
import matplotlib.pyplot as plt

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--phase', '-p', type=float, default=0, help='Definition of a point in time')
    parser.add_argument('--voltage', '-v', type=float, default=100, help='Definition of a effective value')
    parser.add_argument('--frequency', '-f', type=float, default=60, help='Definition of a frequency')
    parser.add_argument('--sampletime', '-t', type=float, default=0.0001, help='Definition of sampletime')

    args = parser.parse_args()

    print('phase : {}\n'.format(args.phase))
    print('voltage : {}\n'.format(args.voltage))
    print('frequency : {}\n'.format(args.frequency))

    t = np.arange(0, 1/60, args.sampletime)
    y1 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t + np.deg2rad(args.phase))
    y2 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t)
    plt.plot(t, y1)
    plt.plot(t, y2)

    plt.grid(True)

    plt.show()

if __name__ == '__main__':
    main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】argparseを使ってコマンドラインで動く簡単なプログラムを作ってみた

ArgumentParser (argparse)

Pythonで書いたプログラムをコマンドラインで実行する時、ArgumentParser (argparse)を用いれば簡単にコマンドライン引数を使うことができます。

コマンドライン引数とは

コマンドライン引数とは次のようなもののことを言います。

python test.py -t 10

使ってみる

瞬時式について

瞬時式は一般に

e=V_{m} \sin(2 \pi f t \pm \phi)

で表されます。今回は

V_{m}[V],f[Hz],\phi[deg]

をコマンドライン変数で変更([]内はプログラム中での単位)し、様々なグラフの描画を試みます。

ソース

ライブラリの読み込み。argparseを読めば使えます。

import argparse

それぞれの引数の初期値などを定義

    parser = argparse.ArgumentParser()
    parser.add_argument('--phase', '-p', type=float, default=0, help='Definition of a point in time')
    parser.add_argument('--voltage', '-v', type=float, default=100, help='Definition of a effective value')
    parser.add_argument('--frequency', '-f', type=float, default=60, help='Definition of a frequency')
    parser.add_argument('--sampletime', '-t', type=float, default=0.0001, help='Definition of sampletime')    
args = parser.parse_args()

指定した数値の表示

    print('phase : {}\n'.format(args.phase))
    print('voltage : {}\n'.format(args.voltage))
    print('frequency : {}\n'.format(args.frequency))

グラフの描画

    t = np.arange(0, 1/60, args.sampletime)
    y1 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t + np.deg2rad(args.phase))
    y2 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t)
    plt.plot(t, y1)
    plt.plot(t, y2)

    plt.grid(True)

    plt.show()

    #plt.savefig("img.png")

コマンドラインで

python test.py -p 45

とすれば

Figure_1.png

を得ます。

コード全体

import numpy as np
import argparse
import matplotlib.pyplot as plt

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--phase', '-p', type=float, default=0, help='Definition of a point in time')
    parser.add_argument('--voltage', '-v', type=float, default=100, help='Definition of a effective value')
    parser.add_argument('--frequency', '-f', type=float, default=60, help='Definition of a frequency')
    parser.add_argument('--sampletime', '-t', type=float, default=0.0001, help='Definition of sampletime')

    args = parser.parse_args()

    print('phase : {}\n'.format(args.phase))
    print('voltage : {}\n'.format(args.voltage))
    print('frequency : {}\n'.format(args.frequency))

    t = np.arange(0, 1/60, args.sampletime)
    y1 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t + np.deg2rad(args.phase))
    y2 = args.voltage * np.sqrt(2) * np.sin(2 * np.pi * args.frequency * t)
    plt.plot(t, y1)
    plt.plot(t, y2)

    plt.grid(True)

    plt.show()

if __name__ == '__main__':
    main()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

なろう小説の評価点から順位を付けてみた

概要

なろう作品の平均評価点を元にした順位です。
ジャンルは全て、四半期と月間ランキングにある小説のみが対象のため、好きな作品が順位に載っていない可能性も多々あります。ご了承下さい。

この記事で得た結果を載せます。

注)2019/12/30現在の、小説家になろうランキング上位作品の「個人的な解析による」順位です。

上位のべ600作品だけを分析しているため、順位が低いものはつまらない、ということでは決してありません。分析対象になったのは既にランキング上位にある選ばれし作品なのです。

結果を見て気分を害される場合があるかもしれませんが、あくまで個人的な解析方法で行った順位付けなのであしからずご了承ください。

では1位から順に発表です。


全データ数:600
評価外件数(評価数500未満):120
重複抜き純データ数:405
平均評価点:93

1位

『本好きの下剋上 ~司書になるためには手段を選んでいられません~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:98.044
評価偏差値:71.9
文中会話率:50%
レビュー数:120

2位

『 お隣の天使様にいつの間にか駄目人間にされていた件』

ジャンル:現実世界〔恋愛〕
評価点:97.989
評価偏差値:71.67
文中会話率:48%
レビュー数:216

3位

『シャングリラ・フロンティア〜クソゲーハンター、神ゲーに挑まんとす〜』

ジャンル:VRゲーム〔SF〕
評価点:97.671
評価偏差値:70.37
文中会話率:49%
レビュー数:45

4位

『エリスの聖杯』

ジャンル:異世界〔恋愛〕
評価点:97.51
評価偏差値:69.71
文中会話率:44%
レビュー数:17

5位

『リビルドワールド』

ジャンル:アクション〔文芸〕
評価点:97.492
評価偏差値:69.63
文中会話率:46%
レビュー数:36

6位

『嘆きの亡霊は引退したい 〜最弱ハンターは英雄の夢を見る〜【Web版】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:97.398
評価偏差値:69.25
文中会話率:27%
レビュー数:23

7位

『オタク同僚と偽装結婚した結果、毎日がメッチャ楽しいんだけど!』

ジャンル:現実世界〔恋愛〕
評価点:97.15
評価偏差値:68.23
文中会話率:32%
レビュー数:2

8位

『貴腐人ローザは陰から愛を見守りたい』

ジャンル:異世界〔恋愛〕
評価点:96.859
評価偏差値:67.03
文中会話率:26%
レビュー数:11

9位

『昏き宮殿の死者の王【Web版】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.823
評価偏差値:66.89
文中会話率:18%
レビュー数:17

10位

『悪役令嬢の執事様 ~俺が育てた彼女はとても可愛い~ (連載版)』

ジャンル:異世界〔恋愛〕
評価点:96.809
評価偏差値:66.83
文中会話率:38%
レビュー数:11

11位

『魔導具師ダリヤはうつむかない』

ジャンル:異世界〔恋愛〕
評価点:96.791
評価偏差値:66.75
文中会話率:44%
レビュー数:22

12位

『Re:ゼロから始める異世界生活』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.777
評価偏差値:66.7
文中会話率:41%
レビュー数:156

13位

『最凶の支援職【話術士】である俺は世界最強クランを従える』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.615
評価偏差値:66.03
文中会話率:53%
レビュー数:20

14位

『狼は眠らない』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.56
評価偏差値:65.81
文中会話率:54%
レビュー数:38

15位

『やり直し令嬢は竜帝陛下を攻略中』

ジャンル:異世界〔恋愛〕
評価点:96.543
評価偏差値:65.74
文中会話率:49%
レビュー数:2

16位

『禁断師弟でブレイクスルー~勇者の息子が魔王の弟子で何が悪い~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.506
評価偏差値:65.58
文中会話率:51%
レビュー数:23

17位

『ガリ勉地味萌え令嬢は、俺様王子などお呼びでない』

ジャンル:異世界〔恋愛〕
評価点:96.345
評価偏差値:64.92
文中会話率:56%
レビュー数:8

18位

『時使い魔術師の転生無双 ~魔術学院の劣等生、実は最強の時間系魔術師でした~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.315
評価偏差値:64.8
文中会話率:37%
レビュー数:64

19位

『悪役令嬢、ブラコンにジョブチェンジします』

ジャンル:異世界〔恋愛〕
評価点:96.299
評価偏差値:64.74
文中会話率:33%
レビュー数:4

20位

『悪役令嬢なのでラスボスを飼ってみました』

ジャンル:異世界〔恋愛〕
評価点:96.196
評価偏差値:64.31
文中会話率:54%
レビュー数:14

21位

『無職転生 - 異世界行ったら本気だす -』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.187
評価偏差値:64.28
文中会話率:22%
レビュー数:163

22位

『ホラー女優が天才子役に転生しました ~今度こそハリウッドを目指します!~』

ジャンル:現実世界〔恋愛〕
評価点:96.075
評価偏差値:63.82
文中会話率:55%
レビュー数:2

23位

『シャバの「普通」は難しい』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.042
評価偏差値:63.68
文中会話率:34%
レビュー数:15

24位

『難攻不落の魔王城へようこそ~デバフは不要と勇者パーティーを追い出された黒魔導士、魔王軍の最高幹部に迎えられる~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:96.039
評価偏差値:63.67
文中会話率:33%
レビュー数:22

25位

『俺は星間国家の悪徳領主!』

ジャンル:宇宙〔SF〕
評価点:95.925
評価偏差値:63.2
文中会話率:32%
レビュー数:12

26位

『転生した大聖女は、聖女であることをひた隠す』

ジャンル:異世界〔恋愛〕
評価点:95.892
評価偏差値:63.06
文中会話率:29%
レビュー数:18

27位

『聖女様はイケメンよりもアンデッドがお好き⁈ 〜 死霊術師として忌み嫌われていた男、英雄の娘に転生して〝癒しの聖女″となる 〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.874
評価偏差値:62.99
文中会話率:36%
レビュー数:2

28位

『乙女ゲー世界はモブに厳しい世界です』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:95.75
評価偏差値:62.48
文中会話率:39%
レビュー数:22

29位

『懐いた後輩がうるさくて寝れないので、意地悪して嫌われようと思う〜それ意地悪じゃなくて惚れさせてますよ?』

ジャンル:現実世界〔恋愛〕
評価点:95.731
評価偏差値:62.4
文中会話率:33%
レビュー数:4

30位

『ティアムーン帝国物語 ~断頭台から始まる、姫の転生逆転ストーリー~』

ジャンル:異世界〔恋愛〕
評価点:95.671
評価偏差値:62.16
文中会話率:32%
レビュー数:9

31位

『転生王女は今日も旗を叩き折る。』

ジャンル:異世界〔恋愛〕
評価点:95.668
評価偏差値:62.14
文中会話率:34%
レビュー数:18

32位

『マジカル★エクスプローラー エロゲの友人キャラに転生したけど、ゲーム知識使って自由に生きる』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.652
評価偏差値:62.08
文中会話率:43%
レビュー数:10

33位

『薬屋のひとりごと』

ジャンル:推理〔文芸〕
評価点:95.652
評価偏差値:62.08
文中会話率:34%
レビュー数:32

34位

『モブ高生の俺でも冒険者になればリア充になれますか?』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:95.624
評価偏差値:61.96
文中会話率:31%
レビュー数:9

35位

『社畜男はB人お姉さんに助けられて――』

ジャンル:現実世界〔恋愛〕
評価点:95.559
評価偏差値:61.7
文中会話率:60%
レビュー数:12

36位

『魔法世界の受付嬢になりたいです』

ジャンル:異世界〔恋愛〕
評価点:95.524
評価偏差値:61.55
文中会話率:39%
レビュー数:17

37位

『最強出涸らし皇子の暗躍帝位争い~帝位に興味ないですが、死ぬのは嫌なので弟を皇帝にしようと思います~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.509
評価偏差値:61.49
文中会話率:45%
レビュー数:9

38位

『私はご都合主義解決担当の王女である』

ジャンル:異世界〔恋愛〕
評価点:95.407
評価偏差値:61.07
文中会話率:31%
レビュー数:2

39位

『元・世界1位のサブキャラ育成日記 ~廃プレイヤー、異世界を攻略中!~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.397
評価偏差値:61.03
文中会話率:44%
レビュー数:33

40位

『転生したらスライムだった件』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.39
評価偏差値:61.0
文中会話率:14%
レビュー数:91

41位

『その悪役令嬢は攻略本を携えている』

ジャンル:異世界〔恋愛〕
評価点:95.362
評価偏差値:60.89
文中会話率:32%
レビュー数:0

42位

『元マフィア幹部の少年、超競争社会の魔法学園で成り上がる~頭脳最強の知略無双~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.309
評価偏差値:60.67
文中会話率:43%
レビュー数:1

43位

『反逆のソウルイーター ~弱者は不要といわれて剣聖(父)に追放されました~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.264
評価偏差値:60.49
文中会話率:23%
レビュー数:16

44位

『王妃レオノーラ』

ジャンル:純文学〔文芸〕
評価点:95.24
評価偏差値:60.39
文中会話率:25%
レビュー数:2

45位

『隣の席になった美少女が惚れさせようとからかってくるがいつの間にか返り討ちにしていた』

ジャンル:現実世界〔恋愛〕
評価点:95.206
評価偏差値:60.25
文中会話率:48%
レビュー数:4

46位

『勇者召喚に巻き込まれたけど、異世界は平和でした』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:95.153
評価偏差値:60.03
文中会話率:53%
レビュー数:74

47位

『落ちこぼれ子竜の縁談 ~閣下に溺愛されるのは想定外ですが!?~』

ジャンル:異世界〔恋愛〕
評価点:95.125
評価偏差値:59.92
文中会話率:28%
レビュー数:2

48位

『村づくりゲームのNPCが生身の人間としか思えない』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:95.081
評価偏差値:59.74
文中会話率:28%
レビュー数:15

49位

『現代社会で乙女ゲームの悪役令嬢をするのはちょっと大変』

ジャンル:現実世界〔恋愛〕
評価点:95.064
評価偏差値:59.67
文中会話率:24%
レビュー数:1

50位

『魔王学院の不適合者 ~史上最強の魔王の始祖、転生して子孫たちの学校へ通う~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.983
評価偏差値:59.33
文中会話率:43%
レビュー数:30

51位

『地球さんはレベルアップした~非匿主義少女の英雄譚~』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.975
評価偏差値:59.3
文中会話率:21%
レビュー数:1

52位

『ずたぼろ令嬢は姉の元婚約者に溺愛される』

ジャンル:異世界〔恋愛〕
評価点:94.94
評価偏差値:59.16
文中会話率:47%
レビュー数:4

53位

『陰の実力者になりたくて!【web版】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.857
評価偏差値:58.82
文中会話率:39%
レビュー数:51

54位

『北の傭兵の息子が南の魔法学院に入学する話。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.843
評価偏差値:58.76
文中会話率:39%
レビュー数:12

55位

『氷の令嬢の溶かし方 ~クールで素っ気ないお隣さんがデレるとめちゃくちゃ可愛い件~』

ジャンル:現実世界〔恋愛〕
評価点:94.829
評価偏差値:58.7
文中会話率:38%
レビュー数:4

56位

『とある策士の三國志(仮)~天下人の軍師~』

ジャンル:歴史〔文芸〕
評価点:94.8
評価偏差値:58.58
文中会話率:32%
レビュー数:9

57位

『信者ゼロの女神サマと始める異世界攻略 クラスメイト最弱の魔法使い』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.79
評価偏差値:58.54
文中会話率:36%
レビュー数:9

58位

『奴隷堕ちした追放令嬢のお仕事』

ジャンル:異世界〔恋愛〕
評価点:94.788
評価偏差値:58.53
文中会話率:45%
レビュー数:2

59位

『Dジェネシス ダンジョンができて3年(旧:ダンジョンが出来て3年。いきなり世界ランク1位になった俺は、会社を辞めてゆるゆると生きてます。)』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.728
評価偏差値:58.29
文中会話率:55%
レビュー数:17

60位

『視える令嬢とつかれやすい公爵【連載】』

ジャンル:異世界〔恋愛〕
評価点:94.708
評価偏差値:58.2
文中会話率:30%
レビュー数:0

61位

『やたらと察しのいい俺は、毒舌クーデレ美少女の小さなデレも見逃さずにグイグイいく』

ジャンル:現実世界〔恋愛〕
評価点:94.691
評価偏差値:58.13
文中会話率:48%
レビュー数:3

62位

『『北土聖伐』顛末記~異世界転生者が奴隷少女を前にして必死過ぎる件~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.686
評価偏差値:58.11
文中会話率:98%
レビュー数:1

63位

『僕は婚約破棄なんてしませんからね』

ジャンル:異世界〔恋愛〕
評価点:94.672
評価偏差値:58.06
文中会話率:55%
レビュー数:7

64位

『ハズレ枠の【状態異常スキル】で最強になった俺がすべてを蹂躙するまで』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.662
評価偏差値:58.01
文中会話率:29%
レビュー数:46

65位

『悪役令嬢は引き籠りたい~転生したら修羅場が多い~』

ジャンル:異世界〔恋愛〕
評価点:94.659
評価偏差値:58.0
文中会話率:24%
レビュー数:4

66位

『婚活ダンジョンちゃん、東京に巣食う』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.641
評価偏差値:57.93
文中会話率:26%
レビュー数:3

67位

『大事なものは失くしてわかる。』

ジャンル:現実世界〔恋愛〕
評価点:94.574
評価偏差値:57.65
文中会話率:39%
レビュー数:1

68位

『おまえだけしか愛せない』

ジャンル:異世界〔恋愛〕
評価点:94.439
評価偏差値:57.1
文中会話率:58%
レビュー数:0

69位

『田中家、転生する。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.43
評価偏差値:57.06
文中会話率:31%
レビュー数:2

70位

『突然パパになった最強ドラゴンの子育て日記 〜かわいい娘、ほのぼのと人間界最強に育つ〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.429
評価偏差値:57.06
文中会話率:39%
レビュー数:1

71位

『ありふれた職業で世界最強』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.394
評価偏差値:56.91
文中会話率:40%
レビュー数:91

72位

『漆黒のダークヒーロー~ヒーローに憧れた俺が、あれよあれよとラスボスに!? ~』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.378
評価偏差値:56.85
文中会話率:43%
レビュー数:4

73位

『許嫁が出来たと思ったら、その許嫁が学校で有名な『悪役令嬢』だったんだけど、どうすればいい?』

ジャンル:現実世界〔恋愛〕
評価点:94.372
評価偏差値:56.82
文中会話率:77%
レビュー数:2

74位

『現代で異能バトルが始まったので、異世界で学んだ魔法で無双していたら、魔法少女が参戦してきた。』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.327
評価偏差値:56.64
文中会話率:44%
レビュー数:2

75位

『サッカーと恋愛 〜普通に生きてきた俺が、ある日サッカーをした事がきっかけで、超美形の兄妹から惚れられてしまうお話〜』

ジャンル:現実世界〔恋愛〕
評価点:94.319
評価偏差値:56.61
文中会話率:37%
レビュー数:1

76位

『転生! 竹中半兵衛 マイナー武将に転生した仲間たちと戦国乱世を生き抜く』

ジャンル:歴史〔文芸〕
評価点:94.302
評価偏差値:56.54
文中会話率:35%
レビュー数:10

77位

『スキル『台所召喚』はすごい!~異世界でごはん作ってポイントためます~』

ジャンル:異世界〔恋愛〕
評価点:94.285
評価偏差値:56.47
文中会話率:36%
レビュー数:2

78位

『ライブダンジョン!』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.279
評価偏差値:56.44
文中会話率:51%
レビュー数:26

79位

『創世のアルケミスト~前世の記憶を持つ私は崩壊した日本で成り上がる~』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:94.255
評価偏差値:56.34
文中会話率:21%
レビュー数:1

80位

『元ホームセンター店員の異世界生活 ~称号《DIYマスター》《グリーンマスター》《ペットマスター》を駆使して異世界を気儘に生きます~』

ジャンル:異世界〔恋愛〕
評価点:94.228
評価偏差値:56.23
文中会話率:42%
レビュー数:2

81位

『影の英雄の日常譚 ~勇者の裏で暗躍していた最強のエージェント、組織が解体されたので、正体隠して人並みの日常を謳歌する~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.194
評価偏差値:56.09
文中会話率:37%
レビュー数:5

82位

『異世界のんびり農家』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.176
評価偏差値:56.02
文中会話率:14%
レビュー数:30

83位

『外れスキル「影が薄い」を持つギルド職員が、実は伝説の暗殺者』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.15
評価偏差値:55.91
文中会話率:47%
レビュー数:5

84位

『もふもふを知らなかったら人生の半分は無駄にしていた【Web版】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.141
評価偏差値:55.88
文中会話率:54%
レビュー数:3

85位

『貧乏令嬢の勘違い聖女伝 ~お金のために努力してたら、王族ハーレムが出来ていました!?~』

ジャンル:異世界〔恋愛〕
評価点:94.12
評価偏差値:55.79
文中会話率:33%
レビュー数:5

86位

『逆行した悪役令嬢は、なぜか魔力を失ったので深窓の令嬢になります』

ジャンル:異世界〔恋愛〕
評価点:94.11
評価偏差値:55.75
文中会話率:31%
レビュー数:1

87位

『人間不信の冒険者達が世界を救うようです』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.105
評価偏差値:55.73
文中会話率:53%
レビュー数:2

88位

『世界最高の暗殺者、異世界貴族に転生する』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.093
評価偏差値:55.68
文中会話率:29%
レビュー数:13

89位

『貴族転生~恵まれた生まれから最強の力を得る』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.089
評価偏差値:55.66
文中会話率:46%
レビュー数:20

90位

『異世界最強の大魔王、転生し冒険者になる』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:94.082
評価偏差値:55.63
文中会話率:34%
レビュー数:0

91位

『出遅れテイマーのその日ぐらし』

ジャンル:VRゲーム〔SF〕
評価点:94.055
評価偏差値:55.52
文中会話率:51%
レビュー数:25

92位

『打撃系鬼っ娘が征く配信道!』

ジャンル:VRゲーム〔SF〕
評価点:94.011
評価偏差値:55.34
文中会話率:36%
レビュー数:2

93位

『冰剣の魔術師が世界を統べる〜世界最強の魔術師である少年は、魔術学院に入学する〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.978
評価偏差値:55.21
文中会話率:49%
レビュー数:2

94位

『モンスターがあふれる世界になったので、好きに生きたいと思います』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:93.899
評価偏差値:54.88
文中会話率:26%
レビュー数:18

95位

『俺は全てを【パリイ】する 〜逆勘違いの世界最強は冒険者になりたい〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.898
評価偏差値:54.88
文中会話率:20%
レビュー数:11

96位

『黄金の経験値』

ジャンル:VRゲーム〔SF〕
評価点:93.883
評価偏差値:54.82
文中会話率:28%
レビュー数:1

97位

『III count Dead END 【旧タイトル転生者のSF戦記。あれ?でもココ転生者の意味無くね?】』

ジャンル:宇宙〔SF〕
評価点:93.88
評価偏差値:54.8
文中会話率:57%
レビュー数:1

98位

『一億年ボタンを連打した俺は、気付いたら最強になっていた~落第剣士の学院無双~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.763
評価偏差値:54.32
文中会話率:38%
レビュー数:2

99位

『悪役令嬢後宮物語』

ジャンル:異世界〔恋愛〕
評価点:93.715
評価偏差値:54.13
文中会話率:56%
レビュー数:13

100位

『メリーさんから家に向かってるって電話がかかってきたんだが、俺今旅行中なんだよな』

ジャンル:現実世界〔恋愛〕
評価点:93.712
評価偏差値:54.12
文中会話率:55%
レビュー数:3

101位

『寝取られ勇者は魔王と駆け落ちする』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.667
評価偏差値:53.93
文中会話率:36%
レビュー数:0

102位

『王女様に婚約を破棄されましたが、おかげさまで幸せです。』

ジャンル:異世界〔恋愛〕
評価点:93.648
評価偏差値:53.85
文中会話率:36%
レビュー数:0

103位

『失格から始める成り上がり魔導師道!~呪文開発ときどき戦記~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.635
評価偏差値:53.8
文中会話率:37%
レビュー数:4

104位

『不死者の弟子~邪神の不興を買って奈落に落とされた俺の英雄譚~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.629
評価偏差値:53.77
文中会話率:29%
レビュー数:2

105位

『私の名はマルカ』

ジャンル:異世界〔恋愛〕
評価点:93.601
評価偏差値:53.66
文中会話率:42%
レビュー数:0

106位

『異世界でも無難に生きたい症候群』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.597
評価偏差値:53.64
文中会話率:51%
レビュー数:4

107位

『聖女の魔力は万能です』

ジャンル:異世界〔恋愛〕
評価点:93.548
評価偏差値:53.44
文中会話率:26%
レビュー数:3

108位

『クラスで陰キャの俺が実は大人気バンドのボーカルな件』

ジャンル:現実世界〔恋愛〕
評価点:93.544
評価偏差値:53.43
文中会話率:40%
レビュー数:1

109位

『異世界で妹天使となにかする。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.52
評価偏差値:53.33
文中会話率:49%
レビュー数:3

110位

『転生した最強の錬金術師は、劣等とされる錬金術で無双する』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.512
評価偏差値:53.29
文中会話率:39%
レビュー数:0

111位

『転生先で捨てられたので、もふもふ達とお料理します   ~お飾り王妃はマイペースに最強です~』

ジャンル:異世界〔恋愛〕
評価点:93.505
評価偏差値:53.27
文中会話率:30%
レビュー数:2

112位

『婚約破棄から始まる悪役令嬢の監獄スローライフ【Web版】』

ジャンル:異世界〔恋愛〕
評価点:93.482
評価偏差値:53.17
文中会話率:57%
レビュー数:15

113位

『復讐を誓った白猫は竜王の膝の上で惰眠をむさぼる』

ジャンル:異世界〔恋愛〕
評価点:93.442
評価偏差値:53.01
文中会話率:35%
レビュー数:0

114位

『ソシャゲキャラになった彼女の日常』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:93.422
評価偏差値:52.92
文中会話率:51%
レビュー数:0

115位

『冒スキ(WEB連載版)』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:93.412
評価偏差値:52.88
文中会話率:20%
レビュー数:7

116位

『幼馴染の妹の家庭教師をはじめたら疎遠だった幼馴染が怖い 〜学年のアイドルが俺のことを好きだなんて絶対に信じられない〜』

ジャンル:現実世界〔恋愛〕
評価点:93.412
評価偏差値:52.88
文中会話率:59%
レビュー数:4

117位

『わたしの幸せな結婚』

ジャンル:異世界〔恋愛〕
評価点:93.403
評価偏差値:52.85
文中会話率:39%
レビュー数:0

118位

『悪役令嬢は天使の皮を被ってます!!』

ジャンル:異世界〔恋愛〕
評価点:93.393
評価偏差値:52.81
文中会話率:40%
レビュー数:0

119位

『貧乏貴族ノードの冒険譚』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.376
評価偏差値:52.74
文中会話率:22%
レビュー数:7

120位

『追放悪役令嬢の旦那様』

ジャンル:異世界〔恋愛〕
評価点:93.354
評価偏差値:52.65
文中会話率:40%
レビュー数:2

121位

『目覚めたら最強装備と宇宙船持ちだったので、一戸建て目指して傭兵として自由に生きたい』

ジャンル:宇宙〔SF〕
評価点:93.346
評価偏差値:52.61
文中会話率:62%
レビュー数:4

122位

『ラスボス、やめてみた ~主人公に倒されたふりして自由に生きてみた~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.334
評価偏差値:52.56
文中会話率:40%
レビュー数:0

123位

『私、能力は平均値でって言ったよね!』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.315
評価偏差値:52.49
文中会話率:34%
レビュー数:31

124位

『メリリース・スペンサーの苦難の日々』

ジャンル:異世界〔恋愛〕
評価点:93.256
評価偏差値:52.24
文中会話率:34%
レビュー数:16

125位

『白豚貴族だったどうしようもない私に前世の記憶が生えた件 (書籍:白豚貴族ですが前世の記憶が生えたのでひよこな弟育てます)』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.242
評価偏差値:52.19
文中会話率:42%
レビュー数:6

126位

『望まぬ不死の冒険者』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.236
評価偏差値:52.16
文中会話率:22%
レビュー数:27

127位

『【驚愕】貧乏さん、奇跡的に手に入れたスマホがワケありすぎて覚醒してしまう・・・からの現代無双』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:93.155
評価偏差値:51.83
文中会話率:46%
レビュー数:3

128位

『神達に拾われた男(改訂版)』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.139
評価偏差値:51.76
文中会話率:55%
レビュー数:0

129位

『公爵令嬢の嗜み』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.121
評価偏差値:51.69
文中会話率:43%
レビュー数:23

130位

『極めた錬金術に、不可能はない。 ~万能スキルで異世界無双~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.055
評価偏差値:51.42
文中会話率:27%
レビュー数:0

131位

『聖女じゃなかったので、王宮でのんびりご飯を作ることにしました』

ジャンル:異世界〔恋愛〕
評価点:93.05
評価偏差値:51.4
文中会話率:37%
レビュー数:0

132位

『魔石グルメ ~魔物の力を食べたオレは最強!~(Web版)』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:93.002
評価偏差値:51.2
文中会話率:46%
レビュー数:10

133位

『転生しまして、現在は侍女でございます。』

ジャンル:異世界〔恋愛〕
評価点:92.977
評価偏差値:51.1
文中会話率:34%
レビュー数:5

134位

『デスマーチからはじまる異世界狂想曲( web版 )』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.964
評価偏差値:51.04
文中会話率:38%
レビュー数:23

135位

『転生して田舎でスローライフをおくりたい』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.928
評価偏差値:50.9
文中会話率:40%
レビュー数:8

136位

『追放者食堂へようこそ! 【書籍2巻発売!!!】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.813
評価偏差値:50.42
文中会話率:49%
レビュー数:7

137位

『ポーション頼みで生き延びます!』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.798
評価偏差値:50.36
文中会話率:29%
レビュー数:14

138位

『暗殺スキルで異世界最強 ~錬金術と暗殺術を極めた俺は、世界を陰から支配する~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.757
評価偏差値:50.19
文中会話率:27%
レビュー数:2

139位

『召喚された先は、私を殺した国でした』

ジャンル:異世界〔恋愛〕
評価点:92.75
評価偏差値:50.17
文中会話率:42%
レビュー数:1

140位

『ニートの逆襲 〜俺がただのニートから魔王と呼ばれるまで〜』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:92.748
評価偏差値:50.16
文中会話率:28%
レビュー数:1

141位

『中卒探索者ですけど今更最強になったのでダンジョンをクリアしたいと思います!』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:92.729
評価偏差値:50.08
文中会話率:55%
レビュー数:1

142位

『ギルド追放された雑用係の下克上 ~超万能な"雑用スキル"で世界最強~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.727
評価偏差値:50.07
文中会話率:50%
レビュー数:3

143位

『D級冒険者の俺、なぜか勇者パーティーに勧誘されたあげく、王女につきまとわれてる』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.723
評価偏差値:50.06
文中会話率:35%
レビュー数:1

144位

『転生貴族の異世界冒険録~自重を知らない神々の使徒~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.677
評価偏差値:49.87
文中会話率:41%
レビュー数:7

145位

『社畜騎士がSランク冒険者に拾われてヒモになる話  ~養われながらスローライフ~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.665
評価偏差値:49.82
文中会話率:41%
レビュー数:5

146位

『誰かこの状況を説明してください』

ジャンル:異世界〔恋愛〕
評価点:92.649
評価偏差値:49.75
文中会話率:44%
レビュー数:3

147位

『アンデッドから始める産業革命』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.648
評価偏差値:49.75
文中会話率:43%
レビュー数:1

148位

『パソコン持ち転生令嬢ですが、推しキャラと永遠の独房生活を満喫中です。』

ジャンル:異世界〔恋愛〕
評価点:92.623
評価偏差値:49.64
文中会話率:26%
レビュー数:3

149位

『嘘が見抜ける令嬢は愛人を脅し婚約者への復讐を決意する』

ジャンル:異世界〔恋愛〕
評価点:92.616
評価偏差値:49.62
文中会話率:51%
レビュー数:0

150位

『没落予定の貴族だけど、暇だったから魔法を極めてみた』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.61
評価偏差値:49.59
文中会話率:45%
レビュー数:7

151位

『不遇職『鍛冶師』だけど最強です ~気づけば何でも作れるようになっていた男ののんびりスローライフ~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.609
評価偏差値:49.59
文中会話率:34%
レビュー数:0

152位

『転生領主の優良開拓〜前世の記憶を生かしてホワイトに努めたら、有能な人材が集まりすぎました〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.605
評価偏差値:49.57
文中会話率:54%
レビュー数:4

153位

『異世界のんびり素材採取生活』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.602
評価偏差値:49.56
文中会話率:34%
レビュー数:3

154位

『俺はまだ、本気を出していない』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.587
評価偏差値:49.5
文中会話率:45%
レビュー数:6

155位

『転生したら小魚だったけど龍になれるらしいので頑張ります』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.451
評価偏差値:48.94
文中会話率:17%
レビュー数:0

156位

『王妃ベルタの肖像 ~うっかり陛下の子を妊娠してしまいました~』

ジャンル:異世界〔恋愛〕
評価点:92.446
評価偏差値:48.92
文中会話率:33%
レビュー数:6

157位

『今度は絶対に邪魔しませんっ!』

ジャンル:異世界〔恋愛〕
評価点:92.443
評価偏差値:48.91
文中会話率:32%
レビュー数:37

158位

『追放されたので洞窟掘りまくってたら、いつのまにか最強賢者になってて、最強国家ができてました』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.437
評価偏差値:48.88
文中会話率:27%
レビュー数:1

159位

『だから俺は〇〇なんかじゃない!~高嶺の花をナンパから助けたら正体がばれました~』

ジャンル:現実世界〔恋愛〕
評価点:92.417
評価偏差値:48.8
文中会話率:58%
レビュー数:4

160位

『外れスキルをもらったけど、敵の攻撃食らってたら最強になってた ~Fランク冒険者は最強英雄候補です~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.414
評価偏差値:48.79
文中会話率:29%
レビュー数:2

161位

『たかが子爵嫡男に高貴な人たちがグイグイきて困る』

ジャンル:異世界〔恋愛〕
評価点:92.38
評価偏差値:48.65
文中会話率:56%
レビュー数:3

162位

『レベル1の最強賢者 ~ 呪いで最下級魔法しか使えないけど、神の勘違いで無限の魔力を手に入れ、最強に ~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.347
評価偏差値:48.51
文中会話率:33%
レビュー数:2

163位

『最弱テイマーはゴミ拾いの旅を始めました。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.312
評価偏差値:48.37
文中会話率:37%
レビュー数:3

164位

『栽培チートで最強菜園 ~え、ただの家庭菜園ですけど?~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.282
評価偏差値:48.24
文中会話率:47%
レビュー数:6

165位

『【web版】最強の魔導士。ひざに矢をうけてしまったので田舎の衛兵になる』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.276
評価偏差値:48.22
文中会話率:51%
レビュー数:9

166位

『くま クマ 熊 ベアー』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.227
評価偏差値:48.02
文中会話率:45%
レビュー数:21

167位

『二度転生した少年はSランク冒険者として平穏に過ごす ~前世が賢者で英雄だったボクは来世では地味に生きる~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.196
評価偏差値:47.89
文中会話率:46%
レビュー数:2

168位

『装備枠ゼロの最強剣士 でも、呪いの装備(可愛い)なら9999個つけ放題』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.192
評価偏差値:47.88
文中会話率:47%
レビュー数:2

169位

『異世界転移で女神様から祝福を! ~いえ、手持ちの異能があるので結構です~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.191
評価偏差値:47.87
文中会話率:38%
レビュー数:16

170位

『婚約破棄された令嬢は野獣辺境伯へ嫁ぐ!【連載版】』

ジャンル:異世界〔恋愛〕
評価点:92.185
評価偏差値:47.85
文中会話率:41%
レビュー数:0

171位

『虐げられた令嬢は、実は最強の聖女~もう愛してくれなくて構いません、私は隣国の民を癒します』

ジャンル:異世界〔恋愛〕
評価点:92.141
評価偏差値:47.67
文中会話率:27%
レビュー数:0

172位

『異世界でスローライフを(願望)』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.124
評価偏差値:47.6
文中会話率:49%
レビュー数:9

173位

『漆黒使いの最強勇者 〜仲間全員に裏切られたので最強の魔物と組みます〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.122
評価偏差値:47.59
文中会話率:51%
レビュー数:11

174位

『目が覚めたら戦国時代‼︎そしてオレは南部晴政⁉︎』

ジャンル:歴史〔文芸〕
評価点:92.094
評価偏差値:47.47
文中会話率:27%
レビュー数:0

175位

『射程極振り弓おじさん』

ジャンル:VRゲーム〔SF〕
評価点:92.093
評価偏差値:47.47
文中会話率:14%
レビュー数:3

176位

『泡になって消えた。』

ジャンル:異世界〔恋愛〕
評価点:92.084
評価偏差値:47.43
文中会話率:15%
レビュー数:0

177位

『世界で唯一の男魔術師』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:92.079
評価偏差値:47.41
文中会話率:45%
レビュー数:1

178位

『レベル1だけどユニークスキルで最強です』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.061
評価偏差値:47.34
文中会話率:42%
レビュー数:17

179位

『盾の勇者の成り上がり』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.01
評価偏差値:47.13
文中会話率:44%
レビュー数:43

180位

『とんでもスキルで異世界放浪メシ』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:92.008
評価偏差値:47.12
文中会話率:36%
レビュー数:109

181位

『転生したら第七王子だったので、気ままに魔術を極めます』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.971
評価偏差値:46.97
文中会話率:35%
レビュー数:0

182位

『実は俺、最強でした? ~転生直後はどん底スタート、でも万能魔法で逆転人生を上昇中!~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.966
評価偏差値:46.95
文中会話率:41%
レビュー数:1

183位

『加護なし令嬢の小さな村』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.964
評価偏差値:46.94
文中会話率:41%
レビュー数:2

184位

『従弟の尻拭いをさせられる羽目になった』

ジャンル:異世界〔恋愛〕
評価点:91.956
評価偏差値:46.91
文中会話率:66%
レビュー数:0

185位

『お姉さま、それちょうだい?』

ジャンル:異世界〔恋愛〕
評価点:91.942
評価偏差値:46.85
文中会話率:49%
レビュー数:2

186位

『「攻略本」を駆使する最強の魔法使い ~〈命令させろ〉とは言わせない俺流魔王討伐最善ルート~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.883
評価偏差値:46.61
文中会話率:35%
レビュー数:3

187位

『俺のスライムから放たれる攻撃魔法は威力がおかしいらしい 〜え、今倒したの人類滅亡クラスの災厄だったんすか?〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.851
評価偏差値:46.48
文中会話率:26%
レビュー数:3

188位

『悪役令嬢は隣国の王太子に溺愛される』

ジャンル:異世界〔恋愛〕
評価点:91.851
評価偏差値:46.48
文中会話率:44%
レビュー数:1

189位

『捨てられ白魔法使いの紅茶生活』

ジャンル:異世界〔恋愛〕
評価点:91.837
評価偏差値:46.42
文中会話率:41%
レビュー数:0

190位

『クズ異能 〜【温度を変える者】の俺が無双するまで〜 WEB版』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:91.832
評価偏差値:46.4
文中会話率:35%
レビュー数:1

191位

『植物魔法チートでのんびり領主生活始めます~前世の知識を駆使して農業したら、逆転人生始まった件~ 』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.814
評価偏差値:46.32
文中会話率:34%
レビュー数:0

192位

『王女殿下はお怒りのようです<WEB版>』

ジャンル:異世界〔恋愛〕
評価点:91.802
評価偏差値:46.27
文中会話率:47%
レビュー数:2

193位

『転生したら宿屋の息子でした。田舎街でのんびりスローライフをおくろう』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.767
評価偏差値:46.13
文中会話率:42%
レビュー数:0

194位

『異世界賢者の転生無双 ~ゲームの知識で異世界最強~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.723
評価偏差値:45.95
文中会話率:30%
レビュー数:4

195位

『転生したら乙女ゲーの世界?いえ、魔術を極めるのに忙しいのでそういうのは結構です。』

ジャンル:異世界〔恋愛〕
評価点:91.717
評価偏差値:45.93
文中会話率:31%
レビュー数:2

196位

『いつでも自宅に帰れる俺は、異世界で行商人をはじめました ~等価交換スキルで異世界通貨を日本円へ~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.713
評価偏差値:45.91
文中会話率:44%
レビュー数:12

197位

『 100人の英雄を育てた最強預言者は、冒険者になっても世界中の弟子から慕われてます』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.708
評価偏差値:45.89
文中会話率:42%
レビュー数:0

198位

『ブラックな騎士団の奴隷がホワイトな冒険者ギルドに引き抜かれてSランクになってました』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.706
評価偏差値:45.88
文中会話率:39%
レビュー数:1

199位

『現代ダンジョンで、生き残るには!? 日本にダンジョンが溢れた結果、最強チートを手に入れたので退職冒険者に!』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:91.689
評価偏差値:45.81
文中会話率:44%
レビュー数:0

200位

『転生賢者の異世界ライフ ~第二の職業を得て、世界最強になりました~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.654
評価偏差値:45.67
文中会話率:36%
レビュー数:5

201位

『転生魔導王は、底辺職の黒魔術士が、実は最強職だと知っている』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.652
評価偏差値:45.66
文中会話率:33%
レビュー数:0

202位

『達人達はおっさんを見過ごせない。 〜追放されたおっさんは『融合』でオリジナルを創りだす〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.59
評価偏差値:45.4
文中会話率:27%
レビュー数:0

203位

『聖女のはずが、どうやら乗っ取られました』

ジャンル:異世界〔恋愛〕
評価点:91.583
評価偏差値:45.38
文中会話率:22%
レビュー数:0

204位

『俺の前世の知識で底辺職テイマーが上級職になってしまいそうな件について 』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.518
評価偏差値:45.11
文中会話率:27%
レビュー数:3

205位

『0歳児スタートダッシュ物語』

ジャンル:異世界〔恋愛〕
評価点:91.491
評価偏差値:45.0
文中会話率:39%
レビュー数:1

206位

『テイマーの限界を超えたみたいなので女の子をテイムして最強パーティーをつくります 〜俺にテイムされると強くなるらしくSランクの獣人も伝説の聖女もエルフの女王も最強の龍王も自分からテイムされにくる〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.483
評価偏差値:44.96
文中会話率:60%
レビュー数:1

207位

『冒険者をクビになったので、錬金術師として出直します! 〜辺境開拓? よし、俺に任せとけ!』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.468
評価偏差値:44.9
文中会話率:50%
レビュー数:2

208位

『攻略難易度ハードモードの地球産ダンジョンは、ボッチが異世界から買う奴隷少女らにとっては生温いようです!!』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:91.458
評価偏差値:44.86
文中会話率:32%
レビュー数:0

209位

『勿論、慰謝料請求いたします!』

ジャンル:異世界〔恋愛〕
評価点:91.455
評価偏差値:44.85
文中会話率:48%
レビュー数:1

210位

『ルイ16世に転生してしまった俺はフランス革命を全力で阻止してマリーと末永くお幸せに暮らしたい』

ジャンル:歴史〔文芸〕
評価点:91.454
評価偏差値:44.85
文中会話率:23%
レビュー数:3

211位

『前世聖女は手を抜きたい よきよき』

ジャンル:異世界〔恋愛〕
評価点:91.404
評価偏差値:44.64
文中会話率:30%
レビュー数:0

212位

『二度と家には帰りません!~虐げられていたのに恩返ししろとかムリだから~』

ジャンル:異世界〔恋愛〕
評価点:91.366
評価偏差値:44.48
文中会話率:32%
レビュー数:1

213位

『神々の加護で生産革命 ~異世界の片隅でまったりスローライフしてたら、なぜか多彩な人材が集まって最強国家ができてました~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.313
評価偏差値:44.27
文中会話率:48%
レビュー数:0

214位

『傷心公爵令嬢レイラの逃避行』

ジャンル:異世界〔恋愛〕
評価点:91.298
評価偏差値:44.21
文中会話率:41%
レビュー数:1

215位

『アラフォー男の異世界通販生活』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.246
評価偏差値:43.99
文中会話率:47%
レビュー数:1

216位

『失格紋の最強賢者 ~世界最強の賢者が更に強くなるために転生しました~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.226
評価偏差値:43.91
文中会話率:34%
レビュー数:9

217位

『精霊の愛子の婚約者』

ジャンル:異世界〔恋愛〕
評価点:91.225
評価偏差値:43.91
文中会話率:36%
レビュー数:0

218位

『真の仲間じゃないと勇者のパーティーを追い出されたので、辺境でスローライフすることにしました』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.222
評価偏差値:43.89
文中会話率:40%
レビュー数:11

219位

『【修復】スキルが万能チート化したので、武器屋でも開こうかと思います』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.186
評価偏差値:43.75
文中会話率:38%
レビュー数:6

220位

『真実の愛に目覚めたので婚約破棄をしました』

ジャンル:異世界〔恋愛〕
評価点:91.16
評価偏差値:43.64
文中会話率:18%
レビュー数:0

221位

『解雇された暗黒兵士(30代)のスローなセカンドライフ』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.135
評価偏差値:43.54
文中会話率:39%
レビュー数:3

222位

『《最強ステータス》を引き継いだ転生少年、勇者の婚約者(お姫様)をうっかり寝取ってしまう』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.079
評価偏差値:43.31
文中会話率:32%
レビュー数:0

223位

『よくわからないけれど異世界に転生していたようです』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.073
評価偏差値:43.28
文中会話率:38%
レビュー数:6

224位

『地味で目立たない私は、今日で終わりにします。』

ジャンル:異世界〔恋愛〕
評価点:91.068
評価偏差値:43.26
文中会話率:43%
レビュー数:0

225位

『おっさん冒険者ケインの善行』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.053
評価偏差値:43.2
文中会話率:39%
レビュー数:14

226位

『転生勇者の気まま旅 ~伝説の最強勇者は第二の人生でさらに強くなるようです~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.031
評価偏差値:43.11
文中会話率:41%
レビュー数:2

227位

『八男って、それはないでしょう! 』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:91.018
評価偏差値:43.06
文中会話率:41%
レビュー数:37

228位

『異世界帰りの大賢者様はそれでもこっそり暮らしているつもりです』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:90.987
評価偏差値:42.93
文中会話率:37%
レビュー数:1

229位

『転生したら兵士だった?!〜赤い死神と呼ばれた男〜』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.902
評価偏差値:42.58
文中会話率:33%
レビュー数:1

230位

『転生貴族、鑑定スキルで成り上がる~弱小領地を受け継いだので、優秀な人材を増やしていたら、最強領地になってた~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.883
評価偏差値:42.5
文中会話率:33%
レビュー数:0

231位

『呪いの魔剣で高負荷トレーニング!? 知られちゃいけない仮面の冒険者』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.868
評価偏差値:42.44
文中会話率:39%
レビュー数:2

232位

『勇者パーティーを追放されたビーストテイマー、最強種の猫耳少女と出会う』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.738
評価偏差値:41.91
文中会話率:45%
レビュー数:2

233位

『新米オッサン冒険者、最強パーティに死ぬほど鍛えられて無敵になる。※コミカライズ連載中!!』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.737
評価偏差値:41.9
文中会話率:40%
レビュー数:1

234位

『駆除人』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.659
評価偏差値:41.58
文中会話率:53%
レビュー数:10

235位

『高一から始める探索者生活』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:90.635
評価偏差値:41.48
文中会話率:37%
レビュー数:1

236位

『世界でただ一人の魔物使い~転職したら魔王に間違われました~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.624
評価偏差値:41.44
文中会話率:48%
レビュー数:0

237位

『最強タンクの迷宮攻略』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.608
評価偏差値:41.37
文中会話率:38%
レビュー数:5

238位

『地獄の業火で焼かれ続けた少年。最強の炎使いとなって復活する。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.589
評価偏差値:41.29
文中会話率:58%
レビュー数:0

239位

『悪役令嬢(予定)らしいけど、私はお菓子が食べたい~ブロックスキルで穏やかな人生目指します~』

ジャンル:異世界〔恋愛〕
評価点:90.584
評価偏差値:41.27
文中会話率:27%
レビュー数:1

240位

『脇役キャラに転生したので知識を活かして暮らしてたら、英雄たちに頼りにされる伝説の錬金術師になってた』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.582
評価偏差値:41.27
文中会話率:38%
レビュー数:1

241位

『魔物を従える"帝印"を持つ転生賢者 ~かつての魔法と従魔でひっそり最強の冒険者になる~【WEB連載版】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.422
評価偏差値:40.61
文中会話率:34%
レビュー数:3

242位

『【web版】ここは俺に任せて先に行けと言ってから10年がたったら伝説になっていた。』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.395
評価偏差値:40.5
文中会話率:49%
レビュー数:11

243位

『ダンジョン暮らし!スキル【ダンジョン図鑑】で楽々攻略?』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:90.268
評価偏差値:39.98
文中会話率:41%
レビュー数:1

244位

『その劣等生、実は最強賢者』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.192
評価偏差値:39.66
文中会話率:28%
レビュー数:0

245位

『のんべんだらりな転生者~貧乏農家を満喫す~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.184
評価偏差値:39.63
文中会話率:44%
レビュー数:0

246位

『姉が剣聖で妹が賢者で』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:90.069
評価偏差値:39.16
文中会話率:36%
レビュー数:0

247位

『すみません、今さら無理です』

ジャンル:異世界〔恋愛〕
評価点:90.048
評価偏差値:39.07
文中会話率:33%
レビュー数:0

248位

『異世界の貧乏農家に転生したので、レンガを作って城を建てることにしました』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.987
評価偏差値:38.82
文中会話率:27%
レビュー数:3

249位

『賢者の孫』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.969
評価偏差値:38.75
文中会話率:53%
レビュー数:13

250位

『地球に出戻りした元勇者は、チートなスキルや魔法を駆使して第3の人生を謳歌する!』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:89.885
評価偏差値:38.4
文中会話率:36%
レビュー数:1

251位

『勇者パーティーを追放された錬金術師、SSSランクダンジョンを創造する(書籍版:追放された錬金術師さん、最強のダンジョンを創りませんか?)』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.839
評価偏差値:38.22
文中会話率:44%
レビュー数:3

252位

『異世界迷宮で奴隷ハーレムを』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.804
評価偏差値:38.07
文中会話率:26%
レビュー数:39

253位

『追放されたけど、スキル『ゆるパク』で無双する』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.759
評価偏差値:37.89
文中会話率:38%
レビュー数:2

254位

『バートレット英雄譚 〜スローライフしたいのにできない弱小貴族奮闘記〜』

ジャンル:ヒューマンドラマ〔文芸〕
評価点:89.522
評価偏差値:36.91
文中会話率:47%
レビュー数:7

255位

『ざまぁ系悪役令嬢の元凶母に転生したようなので、可愛い娘のために早めのお掃除頑張ります』

ジャンル:異世界〔恋愛〕
評価点:89.5
評価偏差値:36.82
文中会話率:20%
レビュー数:0

256位

『現代ダンジョンをチート職「忍者」で無双する。』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:89.405
評価偏差値:36.43
文中会話率:29%
レビュー数:0

257位

『薔薇とすみれ』

ジャンル:異世界〔恋愛〕
評価点:89.366
評価偏差値:36.27
文中会話率:44%
レビュー数:0

258位

『公爵令嬢は婚約破棄したい』

ジャンル:異世界〔恋愛〕
評価点:89.348
評価偏差値:36.2
文中会話率:34%
レビュー数:0

259位

『死に戻り、全てを救うために最強へと至る』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.324
評価偏差値:36.1
文中会話率:35%
レビュー数:3

260位

『乙女ゲームを降りる事にしました』

ジャンル:異世界〔恋愛〕
評価点:89.144
評価偏差値:35.36
文中会話率:43%
レビュー数:0

261位

『砂漠だらけの世界で、おっさんが電子マネーで無双する』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:89.111
評価偏差値:35.23
文中会話率:44%
レビュー数:0

262位

『虐待されていた商家の令嬢は聖女の力を手に入れ、無自覚に容赦なく逆襲する【本編完結】』

ジャンル:異世界〔恋愛〕
評価点:89.058
評価偏差値:35.01
文中会話率:14%
レビュー数:2

263位

『偽聖女と虐げられた公爵令嬢は二度目の人生は復讐に生きる【本編完結】』

ジャンル:異世界〔恋愛〕
評価点:88.996
評価偏差値:34.76
文中会話率:22%
レビュー数:2

264位

『運命の愛なんて知らない』

ジャンル:異世界〔恋愛〕
評価点:88.879
評価偏差値:34.27
文中会話率:49%
レビュー数:0

265位

『没落貴族の嫡男なので好きに生きようと思います~最強な血筋なのにどうしてこうなった~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.67
評価偏差値:33.42
文中会話率:53%
レビュー数:0

266位

『史上最強の大魔王、村人Aに転生する ~村人(規格外)による、普通だけど普通じゃない英雄譚~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.62
評価偏差値:33.21
文中会話率:32%
レビュー数:2

267位

『天空の城を貰ったので異世界で楽しく遊びたい』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.475
評価偏差値:32.62
文中会話率:41%
レビュー数:1

268位

『戦鬼と呼ばれた男、王家に暗殺されたら娘を拾い、一緒にスローライフをはじめる』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.471
評価偏差値:32.6
文中会話率:30%
レビュー数:3

269位

『帰ってきた元奴隷の男』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:88.435
評価偏差値:32.45
文中会話率:47%
レビュー数:1

270位

『2位では意味がないと貴族の家から勘当された少年、実は剣技や魔法、すべてにおいてハイレベル。~無自覚に無双しながら、好き勝手に生きたいと思います~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.4
評価偏差値:32.31
文中会話率:35%
レビュー数:2

271位

『完全回避ヒーラーの軌跡』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:88.367
評価偏差値:32.17
文中会話率:47%
レビュー数:1

272位

『私、興味がないので(連載版)』

ジャンル:異世界〔恋愛〕
評価点:88.338
評価偏差値:32.05
文中会話率:49%
レビュー数:0

273位

『この色に誓って』

ジャンル:異世界〔恋愛〕
評価点:88.153
評価偏差値:31.29
文中会話率:42%
レビュー数:0

274位

『聖女の奇跡』

ジャンル:異世界〔恋愛〕
評価点:88.135
評価偏差値:31.22
文中会話率:49%
レビュー数:0

275位

『裏切りのその末は』

ジャンル:異世界〔恋愛〕
評価点:88.012
評価偏差値:30.72
文中会話率:28%
レビュー数:0

276位

『私はおとなしく消え去ることにします』

ジャンル:異世界〔恋愛〕
評価点:88.007
評価偏差値:30.7
文中会話率:52%
レビュー数:0

277位

『なんとなく歩いてたらダンジョンらしき場所にいた俺の話』

ジャンル:ローファンタジー〔ファンタジー〕
評価点:87.885
評価偏差値:30.19
文中会話率:44%
レビュー数:1

278位

『ニトの怠惰な異世界症候群 ~最弱職〈ヒーラー〉なのに最強はチートですか?~』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:87.846
評価偏差値:30.03
文中会話率:39%
レビュー数:9

279位

『限界レベル1からの成り上がり ~廃棄された限界レベル1の俺、スキル【死体吸収】の力で最強になる~』

ジャンル:アクション〔文芸〕
評価点:87.541
評価偏差値:28.78
文中会話率:33%
レビュー数:0

280位

『貧乏国の悪役令嬢、金儲けに必死になってたら婚約破棄されました』

ジャンル:異世界〔恋愛〕
評価点:87.425
評価偏差値:28.31
文中会話率:31%
レビュー数:1

281位

『不遇らしいけど何とかなりそうなので無難にやってます』

ジャンル:VRゲーム〔SF〕
評価点:87.337
評価偏差値:27.94
文中会話率:57%
レビュー数:0

282位

『ガベージブレイブ【異世界に召喚され捨てられた勇者の復讐物語】』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:87.296
評価偏差値:27.78
文中会話率:36%
レビュー数:0

283位

『婚約破棄をされた令嬢は、全てを見捨てる事にした』

ジャンル:ハイファンタジー〔ファンタジー〕
評価点:86.224
評価偏差値:23.38
文中会話率:30%
レビュー数:0

284位

『ハッピーエンドのそのまえに』

ジャンル:異世界〔恋愛〕
評価点:85.284
評価偏差値:19.52
文中会話率:32%
レビュー数:1

285位

『番だと気づいたのは、婚約破棄した後でした』

ジャンル:異世界〔恋愛〕
評価点:83.634
評価偏差値:12.74
文中会話率:31%
レビュー数:0

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