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

AtCoder Beginner Contest 189 参戦記

AtCoder Beginner Contest 189 参戦記

終了50秒前に書けたE問題を投げたら奇跡的に一発 AC が出て久々に大勝利!

ABC189A - Slot

1分半で突破. 書くだけ.

S = input()

if S[0] == S[1] and S[1] == S[2]:
    print('Won')
else:
    print('Lost')

ABC189B - Alcoholic

3分で突破. double でやるのは嫌な予感がしたので、思考停止で Decimal で. 30秒考えれば100倍すればいいじゃんと気づいただろうに(笑).

from decimal import Decimal

N, X = map(int, input().split())

c = 0
for i in range(N):
    V, P = map(Decimal, input().split())
    c += V * (P / 100)
    if c > X:
        print(i + 1)
        break
else:
    print(-1)

ABC189D - Logical Expression

20分くらいで突破. C問題に一旦敗退してこちらを先に. 普通に AND のほうが OR より優先順位が高いんだろうと思いつつ問題文を読んだら優先順位が同じで、何この簡単な問題ってなった.

N, *S = open(0).read().split()

t = 1
f = 1
for s in S:
    if s == 'AND':
        f = t + f * 2
    elif s == 'OR':
        t = t * 2 + f
print(t)

ABC189C - Mandarin Orange

20分くらいで突破、WA1. N≤104 だから O(N2) だと駄目だよなと思いつつ、順位表を見ると正当者数が多いので、多分テストケースが制限いっぱいに攻めてないんだろうと投げたら AC が出て一安心. 公式解法も O(N2) で、そもそも制限時間も2秒ではなく、1.5秒だったことに気づいて目が白黒してる.

N, *A = map(int, open(0).read().split())

def f(x):
    result = 0
    i = 0
    j = 0
    while i < N:
        while j < N and x <= A[j]:
            j += 1
        result = max(result, x * (j - i))
        i = j + 1
        j = i
    return result

result = 0
for x in set(A):
    result = max(result, f(x))
print(result)

ABC189E - Rotate and Flip

56分で突破. アフィン変換、なんでしたっけ?(まて)

情報学科卒にも関わらずアフィン変換という単語は出てこなかったものの、行列とは気づかずに行列の遷移を書けたおかげで AC できた.

from sys import stdin

readline = stdin.readline

N = int(readline())
XY = [tuple(map(int, readline().split())) for _ in range(N)]
M = int(readline())
op = [readline() for _ in range(M)]
Q = int(readline())
AB = [tuple(map(int, readline().split())) for _ in range(Q)]

# (x, y)
# 1 -> (y, -x)
# 2 -> (-y, x)
# 3 p -> (2p - x, y)
# 4 p -> (x, 2p - y)


def e(x, i):
    result = x[0]
    if x[1] == 1:
        result += XY[i][x[2]]
    elif x[1] == -1:
        result -= XY[i][x[2]]
    return result


x = (0, 1, 0)
y = (0, 1, 1)
applied = 0
result = [None] * Q
for a, b, i in sorted(((AB[i][0], AB[i][1], i) for i in range(Q)), key=lambda x: x[0]):
    while applied < a:
        o = op[applied]
        if o[0] == '1':
            t = x
            x = (y[0], y[1], y[2])
            y = (-t[0], -t[1], t[2])
        if o[0] == '2':
            t = x
            x = (-y[0], -y[1], y[2])
            y = (t[0], t[1], t[2])
        if o[0] == '3':
            p = int(o[2:])
            x = (2 * p - x[0], -x[1], x[2])
        if o[0] == '4':
            p = int(o[2:])
            y = (2 * p - y[0], -y[1], y[2])
        applied += 1
    result[i] = '%d %d' % (e(x, b - 1), e(y, (b - 1)))
print(*result, sep='\n')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習型じゃんけん (Python)

はじめに

  • Python を少し囓った素人が、「学習型じゃんけん」を作ってみました。
  • 学習型といっても、強化学習とかではありません。単純な統計です。
  • 「こう書いた方がスマートだ」など、突っ込み大歓迎です。

環境

  • Jupyter Notebook 6.1.4
  • Python 3.8.5 (default, Sep 3 2020, 21:29:08) [MSC v.1916 64 bit (AMD64)]

アルゴリズム

  • 人間は、単純な選択を短時間に繰り返したときに、無意識に癖が出ます。
  • 直近4回の手の並びの出現頻度を記録して、直近3回の並びから最も頻度の高い次の手を予測とします。
  • 最初に出現履歴と頻度表を適当な乱数で埋めてあります。

使い方

  • 人間が手を入力する前に、プログラムは手の内を見せます。
  • 人間側の手を13の数字で入力します。
    • 機械側の手を見ずに入力するとフェアです。
      • 見たくない場合は、printの行を入力後に移動してください。
    • 0を入力するとゲームを終了します。
  • 人間が手を選ぶと、勝敗を判定し、結果と戦績を表示します。

コード

janken.py
import random as rnd

# 初期化
log = [rnd.randrange(3) for _ in range(4)] # 履歴
frequency = [[[[rnd.randrange(3) for _ in range(3)] for _ in range(3)] for _ in range(3)] for _ in range(3)] # 頻度
handName = { 0:'Goo', 1:'Choki', 2:'Par' } # 手の名前
human = 0 # 人間の勝利数
computer = 0 # 機械の勝利数
draw = 0 # 引き分け数

# メインループ
while True:
    print(f"--- {human + computer + draw + 1} ---")
    # 機械の次の手の算出
    f = frequency[log[1]][log[2]][log[3]] # 人間の次の手の出現頻度
    com = (f.index(max(f)) + 2) % 3 # 最頻手に勝てる手
    print(f"Computer hand: {handName[com]} (secret)") # 先に見せてしまう
    # 人間の次の手の取得
    while True:
        try:
            hand = int(input("[1:Goo, 2:Choki, 3:Par, 0:End]? "))
        except:
            pass
        else:
            if hand >= 0 and hand <= 3:
                hand -= 1
                break
        print("input error")

    if hand < 0: break
    print(f"Human hand: {handName[hand]}")
    #print(f"Computer hand: {handName[com]}") # 後で見せる
    # 判定
    if com == hand:
        result = 'draw'
        draw += 1
    elif com == hand - 1 or com == hand + 2:
        result = 'Human lose!'
        computer += 1
    else:
        result = 'Human win!!'
        human += 1
    print(f"{result} (computer:{computer} human:{human} draw:{draw})")
    # 履歴と頻度の更新
    log.append(hand)
    log = log[-4:]
    frequency[log[0]][log[1]][log[2]][log[3]] += 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【パターン認識】k-means clustering

はじめに

突然ですが、パターン認識についていろいろ書いていこうと思います。

用語について

パターン認識という言葉を使います。いろいろな流派?があり、機械学習、人工知能などいろいろありますが、ここではパターン認識として考えます。それぞれ、突っ込みどころのある用語ですが、まぁそれはおいておいて。

流れ

パターン認識の全てについてですが、モデルを用いて行いことをします。

モデルを用意します。モデルのパラメータを求める必要があります。これを「学習」と呼びます。

モデルを用いて、観測が何であるかを推定します。

K-means algorithm

たくさんのデータがあり、それらが分類されていたとします。
新しく入ってきたデータが、それまでに分類されていたどのクラスに属するかを考える、というタスクを考えます。

そうすると、学習として求めたいモデルは各クラスの代表値、認識は観測値が属するクラスを求める、ことに当たります。

使い方として、

  • たくさんのサンプルを分類する(クラスタリング)
  • 分類したクラスタをもとに、新しいサンプルがどのクラスタに属するか求める

というのを考えてみたいと思います。

サンプルデータ

ここでは、次のように分布している2次元データを考えます。

samples.png

この例だと、何となく4つのクラスタに分類されそうだなー、と思うと思います。
そういう分類を行いたいと思います。

実際には多次元のデータですし、クラスタの分類が明確ではない場合もあります。さらに、クラスタ数がいくつかも普通は分からない状態で使います。(そういう場合の「学習」を「教師無し学習」と呼ぶことがあります。)

これらの学習用のN個の観測を $x_n$ と書くことにします。

今、クラスタ数をK=4とします。各クラスタは代表値をがあるとします。各学習サンプルは、4個あるクラスタのどれかに属します。最も自然な考え方として、代表値に対して最も近くにあるクラスタに属すると考えます。n番目のサンプルが属するクラスタのインデックスk[n] は

k[n] = \arg \min_k ||x_n -  \mu_k||^2

とする。自乗は無くてもあってここでは同じであるが、後の目的関数の最小化で説明しやすいのでつけることにする。

もし、クラスタの代表値が分かっていれば、各サンプルがどこに属するかを求めることができます。一方で、各サンプルがどのクラスタに属するかが分かれば、各クラスタに属するサンプルを用いて代表値を更新することができます。これを繰り返すことが、k-means clustering です。

以下は実行結果です。
最初に乱数で初期の代表値を与えました。初期値の与え方はいろいろありますし、実はかなりデリケートな問題です。それは実際に動かすと出てきますが、ここではとりあえず置いておきます。

iteration.png

☆が各クラスタの代表値の推定値です。繰り返しのステップをくりかえすごとに各クラスタの中心に収束していくことが分かります。

実装は、いろいろあるでしょうが、ここでは変数を全て保持して計算を行う、以下のようにしました。

def kmeans_update_alignment(x:ndarray, mu:ndarray) -> ndarray:
    """
    サンプルがどのクラスタに属するかを求める。
    parameters
    ----------
    x[N, D]: samples
    mu[K, D]: centroids
    """
    N, D = x.shape
    r = np.zeros((N, K))
    for n in range(N):
        r[n, argmin([ sum(v*v for v in x[n, :] - mu[k, :]) for k in range(K)])] = 1
    return r

def kmeans_update_centroid(x:ndarray, r:ndarray) -> ndarray:
    """
    各クラスタの平均値を求める
    Parameters
    ----------
    x, training data (N, D)
    r, alignment information(K, D)
    """
    mu = dot(r.transpose(), x)
    for k in range(K):
        if sum(r[:,k]) < 0.01:
            continue
        mu[k,:] = mu[k,:] / sum(r[:,k])
    return mu

各クラスタの平均を求めるとき、もしそのクラスタに属するサンプルが一つもない場合があり得ます。そのときは更新しないような実装になっています。

 目的関数の最小化

このようにad hoc に更新を行い、無事に収束するところを紹介しましたが、このクラスタリングはいつでも収束します。そのことを確認するために、定式化して考えます。

J = \sum_{n=1}^N \sum_{k=1}^K r_{n,k} || x_n - \mu_k ||^2

を最小化します。

(1) 各サンプルが属するクラスタを求める

$\mu$ を固定したときの$r_{n,k}$の最適化です。かならず目的関数が減少します。

(2) クラスタの中心の更新

$r_{n,k}$固定したときに最適な目的関数を最小にする$\mu_k$を求めます。これは、各クラスタごとに独立に最適化することができます。

J = \sum_{x_n\in C_1} || x_n - \mu_k||^2 + \cdots + \sum_{x_n\in C_1} || x_n - \mu_k||^2

自乗誤差を最小にするベクトルは平均ベクトルです。形をみると、各クラスタを正規分布と思い、その正規分布の平均の最尤推定値を求めることと同じ計算をしていますね。

\hat{\mu_k} = \frac{1}{N_k}\sum_{x_n \in C_k} x_n

以上から、単調に減少し、かつ、これは下限があるので、結果として必ず収束することが分かります。(「収束」の定義が何かはおいておいて^^;)先の計算での目的関数は、下記のように収束しています。最初は代表値が遠くにあるので目的関数は大きいですが、あとは単調に収束していきます。k-means ではサンプルがどのクラスタに属するかという01なので、全てのサンプルのクラスタへの帰属が更新により変化しなくなった時が、クラスタリングの処理が終了したときになります。

distortion.png

行列による表現

実は、これらの計算を行列で書くことができます。さきに書いたpython コードは行列表現で書いています。これらは何となく昔からある慣習?なのかな、と思っていますが、ここでは一応紹介だけしました。(時間があれば後で式も追加)

まとめ

K-means 法の概要と実装を復習しました。いろいろ思い出してきました。

緒言

アルゴリズムについては:

  • 初期値の設定について:初期値はデリケートな問題です。クラスタを徐々に増やしていく方法に、LBGアルゴリズムというのがあります。余力があれば紹介したい
  • 制約条件がある場合:分布について、ノルムが1など、制約がある場合は応用例として考えらえれる。spherical k-means と呼ばれるものがこれに当たるのか?

実際にいろいろ動かすと分かることですが:

サンプル数が多いと収束までのステップが長くなる。(境界のサンプルの取り合いでセントロイドが微妙に更新される)
- サンプルが割り当てられないクラスタもある。
- シミュレーションの場合、ガウス分布の平均値に収束するわけではない。(サンプルの割り当ての最適性とは異なる。生成モデルが異なるので。)

  • 実装を公開
  • グラフの余白の調整

等々。

今日はここまで。次回はGMMの予定。
(2021/01/23)

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

【パターン認識】k-means

はじめに

突然ですが、パターン認識についていろいろ書いていこうと思います。

用語について

パターン認識という言葉を使います。いろいろな流派?があり、機械学習、人工知能などいろいろありますが、ここではパターン認識として考えます。それぞれ、突っ込みどころのある用語ですが、まぁそれはおいておいて。

流れ

パターン認識の全てについてですが、モデルを用いて行いことをします。

モデルを用意します。モデルのパラメータを求める必要があります。これを「学習」と呼びます。

モデルを用いて、観測が何であるかを推定します。

K-means algorithm

たくさんのデータがあり、それらが分類されていたとします。
新しく入ってきたデータが、それまでに分類されていたどのクラスに属するかを考える、というタスクを考えます。

そうすると、学習として求めたいモデルは各クラスの代表値、認識は観測値が属するクラスを求める、ことに当たります。

使い方として、

  • たくさんのサンプルを分類する(クラスタリング)
  • 分類したクラスタをもとに、新しいサンプルがどのクラスタに属するか求める

というのを考えてみたいと思います。

サンプルデータ

ここでは、次のように分布している2次元データを考えます。

samples.png

この例だと、何となく4つのクラスタに分類されそうだなー、と思うと思います。
そういう分類を行いたいと思います。

実際には多次元のデータですし、クラスタの分類が明確ではない場合もあります。さらに、クラスタ数がいくつかも普通は分からない状態で使います。(そういう場合の「学習」を「教師無し学習」と呼ぶことがあります。)

これらの学習用のN個の観測を $x_n$ と書くことにします。

今、クラスタ数をK=4とします。各クラスタは代表値をがあるとします。各学習サンプルは、4個あるクラスタのどれかに属します。最も自然な考え方として、代表値に対して最も近くにあるクラスタに属すると考えます。

k[n] = argmin_k (x_n, mu_k)

もし、クラスタの代表値が分かっていれば、各サンプルがどこに属するかを求めることができます。一方で、各サンプルがどのクラスタに属するかが分かれば、各クラスタに属するサンプルを用いて代表値を更新することができます。これを繰り返すことが、k-means clustering です。

以下は実行結果です。
最初に乱数で初期の代表値を与えました。初期値の与え方はいろいろありますし、実はかなりデリケートな問題です。それは実際に動かすと出てきますが、ここではとりあえず置いておきます。

iteration.png

☆が各クラスタの代表値の推定値です。繰り返しのステップをくりかえすごとに各クラスタの中心に収束していくことが分かります。

実装は、いろいろあるでしょうが、ここでは変数を全て保持して計算を行う、以下のようにしました。

def kmeans_update_alignment(x:ndarray, mu:ndarray) -> ndarray:
    """
    サンプルがどのクラスタに属するかを求める。
    parameters
    ----------
    x[N, D]: samples
    mu[K, D]: centroids
    """
    N, D = x.shape
    r = np.zeros((N, K))
    for n in range(N):
        r[n, argmin([ sum(v*v for v in x[n, :] - mu[k, :]) for k in range(K)])] = 1
    return r

def kmeans_update_centroid(x:ndarray, r:ndarray) -> ndarray:
    """
    各クラスタの平均値を求める
    Parameters
    ----------
    x, training data (N, D)
    r, alignment information(K, D)
    """
    mu = dot(r.transpose(), x)
    for k in range(K):
        if sum(r[:,k]) < 0.01:
            continue
        mu[k,:] = mu[k,:] / sum(r[:,k])
    return mu

各クラスタの平均を求めるとき、もしそのクラスタに属するサンプルが一つもない場合があり得ます。そのときは更新しないような実装になっています。

 目的関数の最小化

このようにad hoc に更新を行い、無事に収束するところを紹介しましたが、このクラスタリングはいつでも収束します。そのことを確認するために、定式化して考えます。

J = \sum_{n=1}^N \sum_{k=1}^K r_{n,k} || x_n - \mu_k ||^2

を最小化します。

(1) 各サンプルが属するクラスタを求める

$\mu$ を固定したときの$r_{n,k}$の最適化です。かならず目的関数が減少します。

(2) クラスタの中心の更新

$r_{n,k}$固定したときに最適な目的関数を最小にする$\mu_k$を求めます。これは、各クラスタごとに独立に最適化することができます。

J = \sum_{x_n\in C_1} || x_n - \mu_k||^2 + \cdots + \sum_{x_n\in C_1} || x_n - \mu_k||^2

自乗誤差を最小にするベクトルは平均ベクトルです。形をみると、各クラスタを正規分布と思い、その正規分布の平均の最尤推定値を求めることと同じ計算をしていますね。

\hat{\mu_k} = \frac{1}{N_k}\sum_{x_n \in C_k} x_n

以上から、単調に減少し、かつ、これは下限があるので、結果として必ず収束することが分かります。(「収束」の定義が何かはおいておいて^^;)先の計算での目的関数は、下記のように収束しています。最初は代表値が遠くにあるので目的関数は大きいですが、あとは単調に収束していきます。k-means ではサンプルがどのクラスタに属するかという01なので、全てのサンプルのクラスタへの帰属が更新により変化しなくなった時が、クラスタリングの処理が終了したときになります。

distortion.png

行列による表現

実は、これらの計算を行列で書くことができます。さきに書いたpython コードは行列表現で書いています。これらは何となく昔からある慣習?なのかな、と思っていますが、ここでは一応紹介だけしました。(時間があれば後で式も追加)

まとめ

K-means 法の概要と実装を復習しました。いろいろ思い出してきました。

緒言

アルゴリズムについては:

  • 初期値の設定について:初期値はデリケートな問題です。クラスタを徐々に増やしていく方法に、LBGアルゴリズムというのがあります。余力があれば紹介したい
  • 制約条件がある場合:分布について、ノルムが1など、制約がある場合は応用例として考えらえれる。spherical k-means と呼ばれるものがこれに当たるのか?

実際にいろいろ動かすと分かることですが:

サンプル数が多いと収束までのステップが長くなる。(境界のサンプルの取り合いでセントロイドが微妙に更新される)
- サンプルが割り当てられないクラスタもある。
- シミュレーションの場合、ガウス分布の平均値に収束するわけではない。(サンプルの割り当ての最適性とは異なる。生成モデルが異なるので。)

等々。

今日はここまで。次回はGMMの予定。
(2021/01/23)

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

Lambda(Python)でDynamo DBに接続しようとした際に"Unable to marshal response: Object of type SSLError is not JSON serializable"が出た件について

下記の記事を参考にして、Amplifyプロジェクト内にDynamo DBに接続するLambda関数を実装していたところ、タイトルにあるエラーが出ました。

AWS API GatewayとLambdaでDynamoDB操作

この記事では、その対処方法を備忘録代わりに記載します。

環境

  • Windows10 20H2
  • WSL2 - Ubuntu-20.04
  • Amplify CLI 4.41.0
  • Python 3.8.5
  • pipenv 2020.11.15

症状

冒頭の記事を参考にして、一部を書き換えながら試していたとき、タイムアウトのエラーが起こりました。

START RequestId: 04359089-637f-48f3-aa2b-e7649c53e4d1 Version: $LATEST
{'OperationType': 'SCAN'}
END RequestId: 04359089-637f-48f3-aa2b-e7649c53e4d1
REPORT RequestId: 04359089-637f-48f3-aa2b-e7649c53e4d1  Duration: 25025.37 ms   Billed Duration: 25000 ms   Memory Size: 128 MB Max Memory Used: 73 MB  Init Duration: 350.04 ms    
2021-01-23T11:46:56.775Z 04359089-637f-48f3-aa2b-e7649c53e4d1 Task timed out after 25.03 seconds

Lambda関数デフォルトのタイムアウトが25秒だったので、それを+1分して実行したところ、下記のエラーが根本のエラーであることがわかりました。

Response:
{
  "errorMessage": "Unable to marshal response: Object of type SSLError is not JSON serializable",
  "errorType": "Runtime.MarshalError"
}

Request ID:
"7dceb7ee-1a1c-4361-b22e-00e489d1264f"

Function logs:
START RequestId: 7dceb7ee-1a1c-4361-b22e-00e489d1264f Version: $LATEST
{'OperationType': 'SCAN'}
Error Exception.
[ERROR] Runtime.MarshalError: Unable to marshal response: Object of type SSLError is not JSON serializableEND RequestId: 7dceb7ee-1a1c-4361-b22e-00e489d1264f
REPORT RequestId: 7dceb7ee-1a1c-4361-b22e-00e489d1264f  Duration: 25721.98 ms   Billed Duration: 25722 ms   Memory Size: 128 MB Max Memory Used: 74 MB  Init Duration: 388.41 ms    

SSL関係でエラーが出ているようですね。

原因

調べてみると、以下の記事にヒントがあり、certifiのバージョンによって不具合があるとのことでした。

Lambdaでboto3を使ってDevice Shadowを取得しようとしたらSSLでエラーが起きたから応急処置をする

自身の環境を確認してみると、certifi==2019.11.28でした。

$ pipenv graph
...
certifi==2019.11.28
...

対処

ということで、Pipfileに最新のバージョンを設定してみました。

Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
certifi = "2020.12.5"

[requires]
python_version = "3.8"

Lambdaのテストを実行してみたところ、問題なくDynamodbに接続することができました。

START RequestId: f7c621d7-ee5b-41ca-a4c3-5ab78c8fa103 Version: $LATEST
Received event: "{\r\n\"OperationType\": \"SCAN\"\r\n}"
END RequestId: f7c621d7-ee5b-41ca-a4c3-5ab78c8fa103
REPORT RequestId: f7c621d7-ee5b-41ca-a4c3-5ab78c8fa103  Duration: 283.60 ms Billed Duration: 284 ms Memory Size: 128 MB Max Memory Used: 76 MB  Init Duration: 353.24 ms

まとめ

自分の環境だけ、たまたま古いパッケージが入っていたのか、Amplifyで作ったパッケージがすべてこうなるのかはよくわかりません。

Pythonを触るのはLambdaを書くときだけなので、解決にも時間がかかってしまいました。ただ、このトラブルシュートでPythonにもAmplifyにも少し詳しくなることができたのでよかったです。

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

BeautifulSoup,Selenium備忘録

はじめに

スクレイピングの基本操作を備忘録用として投稿します。

対象ページ、クラスのスクレイピング

qiita.rb
import sys
from selenium import webdriver
import os
from bs4 import BeautifulSoup


options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
browser = webdriver.Chrome('chromedriver',options=options) 
# 対象のWebページURLを宣言します 
url = "https://qiita.com/" 
# 対象URLを取得 
browser.get(url)
html = browser.page_source.encode('utf-8')
soup = BeautifulSoup(html,'html.parser')
list = soup.find_all(class_ = "css-1laxd2k")

print(list)

結論こちらに対象URLと取得したいクラスやidをいれたら取得できる。
本来requests.get()で取得した方が楽ですけどね。

BeautifulSoupとは

requestsなどによってHTMLのデータをスクレイピングした後に
そのHTMLを整形するために使用。
所謂BeautifulSoupだけではスクレイピングはできない。
これ使えばスクレイピングしたデータに色々できる

Seleniumとは

Webページの自動化を行うためのフレームワーク
今回はoptionに色々やっているのでclickなどによるページ遷移も
すぐできたり

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

Djangoをとりあえず動かす ~pythonインストールからstartprojectまで~

どんな記事?

  • Djangoの環境構築手順書を毎回忘れるので、自分用メモとして作成。
  • 環境構築が完了した状態から遡って手順を確認したので、抜け漏れがあるかも。

環境構築手順

python インストール

何はともあれpythonのインストール。
(参照|https://www.python.jp/install/windows/install.html)

今回はubuntsu経由?でインストールしたので、下コマンドを実行。
sudo apt-get install python3

pip インストール

pipのインストール。
(参照|https://qiita.com/suzuki_y/items/3261ffa9b67410803443)

ubuntsuでpipをインストールする前には、ubuntsuの付属パッケージ?の更新をしておいた方が良いらしい。
sudo apt-get update

pipインストールは以下。
sudo apt-get install python3-pip

仮想環境構築

必須ではないが、設定しておくと何かと便利。
個人開発では必要ないかもしれないが、製品開発では動作環境が問題になることがあるので必須だと思う。
(参考|https://www.acrovision.jp/career/?p=2254)

今回はvenvで仮想環境を構築した。
(参考|https://qiita.com/fiftystorm36/items/b2fd47cf32c7694adc2e)

Django インストール

pipでDjangoのライブラリをインストールするだけでOK。
注意点として、作成した仮想環境のディレクトリを覗いている状態でインストールする。
pip3 install django

プロジェクト作成

プロジェクトを作成したいディレクトリへ移動する。

プロジェクトを作成するコマンドは以下。
jango-admin startproject [プロジェクト名]

プロジェクトの作成を確認

Djangoのプロジェクトは、初期状態でもアクセスできるページが存在する。
サーバーを起動して、指定されたURLにアクセスする。
python3 manage.py runserver

つまづいたところ

adminページへのアクセス不可

問題
初期状態では、アクセスできていたadminページに対して、ある段階から以下のようなメッセージが出てアクセス不可になった。

エラーメッセージ:「no such table: django_session」

原因
viewに新たなメソッドを作成したが、DBに登録されていなかった

対策
migrationする

python manage.py makemigrations
python manage.py migrate

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

Python で保存する Excel ファイルのハッシュ値を固定する

(結論だけ知りたい方は「解決方法」のセクションまでお進みください)

はじめに

Excel ファイルは、表形式の情報を整理する用途では(特に「テキストファイル」に馴染みのない方たちにとっては)もっとも親しまれている形式です。

いっぽうで各種プログラミング言語から読み書きするためのライブラリも存在しており、こと「非エンジニアに情報を提示する」という目的に絞れば、Excel ファイルで出力するのは悪い選択肢ではありません。

ところが、そのファイルを Git 管理しようとするとさぁ大変。中身が全く同じでも、バイナリレベルでは異なる Excel ファイルが生成されてしまうことがあるのです。おかげで「何も変更していないのにコンフリクトした!」という事象が多々発生します。

分かっています、Excel ファイルを Git 管理しようとするのが正気の沙汰ではないことくらい。でもそういう案件があったんですよ。本当に。そのときに学んだことを供養させてください。

素朴に書き出すと何が問題か

ここでは Python 3 で openpyxl(記事執筆時点で 3.0.6 が最新)を使ってファイルを書き出すことにします。バージョン管理が Mercurial なので一瞬面食らいますが、今でもきちんとメンテされているようですし、Pandas でも採用されているので問題ないでしょう。

こんな感じに、ちょっと間を置いて2回ファイルを書き出してみます。

from time import sleep

from openpyxl import Workbook


if __name__ == "__main__":
    book = Workbook()
    book.active["A1"] = "Hello world"

    book.save("hello1.xlsx")
    sleep(1)
    book.save("hello2.xlsx")

書き出したファイルのハッシュ値を比較してみると、異なっているのが分かります。

$ sha1sum hello*.xlsx
091a1922ac5f2dc58e91ac3bbae32e3ca58c5ca5  hello1.xlsx
ed51a9901eed0a5d2f2c21dd6664bb8a07a9563c  hello2.xlsx

ファイルの中身が同じなら、ハッシュ値も全くおなじになっていてほしいですよね。
それをどう実現するか、というのが本記事の趣旨です。

解決方法:タイムスタンプの固定

種明かしをしてしまえば、ファイルコンテンツに含まれるタイムスタンプを固定するというのが解決方法になります。テキストファイルであれば OS が管理するファイル属性にタイムスタンプが含まれるだけですが、Excel の場合はファイルの中身にもタイムスタンプが書き込まれているんですね。

下記のようにすれば、常に完全に同一な(バイナリレベルで一致する)Excel ファイルが生成できます。

from datetime import datetime
from time import sleep
from zipfile import ZipFile

from openpyxl import Workbook


def save_workbook(book: Workbook, path: str) -> None:
    # タイムスタンプをこの値に固定する
    timestamp = datetime(1980, 1, 1, 00, 00, 00)

    # Excel 管理のタイムスタンプ固定
    book.properties.created = timestamp
    book.properties.modified = timestamp

    # ファイル保存
    book.save(path)

    # ZIP 管理のタイムスタンプ固定
    with ZipFile(path, mode="a") as f:
        for info in f.infolist():
            info.date_time = timestamp.timetuple()[:6]
            f.fp.seek(info.header_offset)
            # ファイルの途中だが、タイムスタンプ変更してもヘッダー長変わらないので OK
            f.fp.write(info.FileHeader())
        f._didModify = True


if __name__ == "__main__":
    book = Workbook()
    book.active["A1"] = "Hello world"

    save_workbook(book, "hello_fixed1.xlsx")
    sleep(1)
    save_workbook(book, "hello_fixed2.xlsx")

固定できていますね!

$ sha1sum hello_fixed*.xlsx
49f5b2f7b9e637ded7c3d81bdf2522b0d14d2e79  hello_fixed1.xlsx
49f5b2f7b9e637ded7c3d81bdf2522b0d14d2e79  hello_fixed2.xlsx

処理内容について、簡単に解説したいと思います。

なお前提知識として「xlsx ファイルは XML ファイルを Zip で固めたもの」というのを知っておくといいでしょう。

$ unzip -d hello_fixed1.xlsx.d hello_fixed1.xlsx
Archive:  hello_fixed1.xlsx
  inflating: hello_fixed1.xlsx.d/docProps/app.xml
  inflating: hello_fixed1.xlsx.d/docProps/core.xml
  inflating: hello_fixed1.xlsx.d/xl/theme/theme1.xml
  inflating: hello_fixed1.xlsx.d/xl/worksheets/sheet1.xml
  inflating: hello_fixed1.xlsx.d/xl/styles.xml
  inflating: hello_fixed1.xlsx.d/_rels/.rels
  inflating: hello_fixed1.xlsx.d/xl/workbook.xml
  inflating: hello_fixed1.xlsx.d/xl/_rels/workbook.xml.rels
  inflating: hello_fixed1.xlsx.d/[Content_Types].xml

参考:

Excel 管理のタイムスタンプ固定

1つ目は Excel のレイヤーで管理されているプロパティで、作成日時と更新日時が記録されています。

技術的に言うと docProps/core.xml というファイルに記載されています。ファイルの中身を覗いてみるとこんな感じ(整形済み)。

<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <dc:creator>openpyxl</dc:creator>
    <dcterms:created xsi:type="dcterms:W3CDTF">1980-01-01T00:00:00Z</dcterms:created>
    <dcterms:modified xsi:type="dcterms:W3CDTF">1980-01-01T00:00:00Z</dcterms:modified>
</cp:coreProperties>

openpyxl ではこれは Workbook#properties.created, Workbook#properties.modified 属性で設定されるようですので(ソース: workbook/workbook.py#L67, packaging/core.py#L76-78)、それを保存直前に上書きしてあげればいいという寸法です。

book.properties.created = timestamp
book.properties.modified = timestamp

book.save(path)
# ...

ZIP 管理のタイムスタンプ固定

上記によって「ZIP 圧縮前」のファイルのレベルでは完全に固定できましたが、最終的な xlsx ファイルはまだ固定しきれていません。

それは ZIP ファイルのレイヤーでもタイムスタンプを保持しているからです。

そのタイムスタンプを固定するために、いったん Workbook#save() したあとに zipfile.Zipfile を使ってファイルを開き直してタイムスタンプを書き換えます(save() をいじって保存処理に手を加えようとしたのですが、思ったより該当処理が深かったため、トリッキーなこの形になりました)。

なお ZIP ファイルの形式は MS-DOS 由来なため、タイムスタンプの起点は1980年1月1日です。それ以前の日付は保持することができないため、固定値を UNIX timestamp の起点である1970年1月1日にするとエラーになるので注意してください。

まずファイルを mode="a" で開きます("w" だと中身が消えてしまいます)。

# ...
book.save(path)

with ZipFile(path, mode="a") as f:

開くと各ファイルのプロパティ情報が zipfile.ZipInfo クラスで保持されているので、この date_time 属性を変更します。

     for info in f.infolist():
        info.date_time = timestamp.timetuple()[:6]

変更した値を反映する必要があるのですが、注意点として、ZIP のファイルプロパティは2箇所に重複して書き込まれています。齟齬がある場合は後者が尊重されるようですが、ファイルを固定するという意味では両方に反映する必要がありますね。

  • ローカルファイルヘッダー……各ファイルエントリーの先頭に書き込まれたプロパティ
  • セントラルディレクトリ……ZIP ファイル全体の末尾に書き込まれたプロパティ

参考:

ローカルファイルヘッダーについては、各ファイルエントリーごとに該当箇所まで seek して write します。

        f.fp.seek(info.header_offset)
        # ファイルの途中だが、タイムスタンプ変更してもヘッダー長変わらないので OK
        f.fp.write(info.FileHeader())

セントラルディレクトリについては ZipFile#_didModifyTrue に設定しておくことで、自動で反映されるようです(ソース: Lib/zipfile.py#L1821-L1825)。

    f._didModify = True

おわりに

以上、ファイルの中身が同じならハッシュ値も同じになるように保存する方法を解説しました。

ただし執筆にあたっては Excel や ZIP の仕様には当たっておらず、実装レベルで試行錯誤した結果となります。書き出しに用いたソフトウェアのバージョンや対象ファイルのサイズなどによってはうまく動かない可能性があることをご理解ください。

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

【合格体験記】Pythonエンジニア認定基礎試験(2021/01/23)

合格しました!

1/10に受けた基本情報はおそらく合格しました。
しかし、選択したPythonでひどい正答率をたたき出してしまいました(泣)
悔しかったのでPythonエンジニア認定基礎試験を受験し、無事雪辱を果たしました^^
この記事では、Pythonエンジニア認定基礎試験の申込方法・勉強法・所感を紹介します。

筆者のバックグラウンド

文系大学出身の20卒です。都内のIT企業で働いています。

◆業務経歴
 4月    主にPython初級レベルの新人研修を受ける
 5月    部署でJava研修を受ける
 6月~   環境移行・オープン化案件でテストや運用監視業務を担当

入社前や新人研修でPythonの基礎を学び、簡単な関数を組めるレベルになりました。
しかし昨年5月でPythonの勉強を中断したため、年末にはPythonの書き方を大方忘れてしまっている状態でした。
一方、Pythonの勉強を中断している間にJavaを勉強し、JavaSilverの資格を取得しました。
つまり資格勉強開始時には、Python初心者・Java中級者程度のプログラミングスキルでした。

申し込み方法

OdesseyIDの作成
1分ほどで登録が完了する簡単なものでした。
申し込みには必要ありませんが、受験時や結果確認に必要です。

試験会場を探す
ざっと見たところ、パソコン教室が会場になっている場合が多いようです。
毎日受験できる会場もあれば、土日のみの会場もあります。

会場を選択すると、「※受験希望の方は、試験会場へお問い合わせください」と書いてありますが、結局教室のHP(ISAオンラインショップ等)から直接申し込むことになります。
会場と日時を選択すると、支払い方法の案内が来ます。銀行振込を指定されました。
支払いを済ませたところで申し込み完了です!

勉強法

使用教材は3つです。

スッキリわかるPython入門(学習期間:3日)
Pythonが全くの初心者ではなかったうえに、プログラミングの基礎知識はJavaで身につけていたので一瞬で終わりました。
この教材があればPythonについて一通りは学べるので、プログラミング初心者・他言語経験者におすすめです。
Pythonの知識が少しでもある人には少々物足りない内容だと思います。

Pythonチュートリアル第3版(学習期間:1週間)
Pythonエンジニア認定基礎試験公式の教材で、試験概要にてどの章から何問出るかまで公開されています。

試験概要:https://www.pythonic-exam.com/exam/basic

これが本当に曲者。内容が難しいというか、翻訳がおかしいというか、とにかくわかりづらくて腹が立ちます。
半年前に購入したときは、本当に読めなくて途中でギブアップしました。
Javaをしっかり学んだ今でも7割くらいしか理解できません。

でもそれでいいんです。
試験対策という観点だと、この教材は流し読み程度で十分。
事実試験概要の動画で公式の人が、「この本は学校の教材でいう教科書だから、勉強は問題集(他の教材)を使ってください」とおっしゃっていますから。

if この教材を読んだときに言っていることが1ミリもわからない:
    スッキリわかるPython入門等他教材で再度勉強
else:
    教材での勉強終了

でいきましょう!

DIVE INTO EXAM(回数:3回)
会員登録するだけで受けられる無料の模擬試験です。
1回目を受験した時点で800点(合格700点)だった時点で察しました。

(簡単と聞いていたけどここまでとは…)

他言語履修してたらPython全く分からなくても合格できるのでは、レベルです。
結局、2回目も800点、3回目925点で試験に臨みました。

所感

模試よりは多少難しかったですが、模試で合格点を取れれば十分合格できると思います。
60分40問の試験でしたが、見直しも含め30分ほどで終わり、結果875点でした。

気を付けるべきことが2点ほどあります。

メモ用紙は使えない
まあメモするほどのこともないですけど、メモできるつもりでいたのでちょっと動揺しました。

地味に「複数選べ」がある
これは見直しの時に気が付いて一命を取り留めました。
複数選択で1つしか選んでいなくても通知されないのでご注意を!

おわりに

Pythonって簡単に使えるのが売りだから、なかなか中上級レベルの試験を作るのは難しいんですかね…
もうちょっと試験の質を高めていただきたいところですね。

最後まで読んでいただき、ありがとうございました。
皆さんが合格できますように!

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

Googleの検索結果をpecoりたい「Google Fuzzy Search」の開発

はじめに

pecoとかfzfとかskimとかいろんなfuzzy findツールがある中、 googleの検索結果だけfuzzy findするツールがなかったので作ってみました。

Google Fuzzy Search とは

gfzs

とってもシンプルなツールで 「標準入力から受けたjsonをインクリメンタルサーチして、選択したアイテムをブラウザで開くためのツールです。jsonはgooglerで用意したものを使います。」

百聞は一見にしかずということで動いているものをご覧ください。(拡大してご覧ください。)

gfzs

fixtures/tokyo.json東京 をキーワードにgooglerで調べたものを保存しておいて、それを標準入力として渡して使ってます。 QUERY> となっている部分に絞りたい単語 コロナ を入れて検索結果を絞り、Enter を押してブラウザで開いている様子です。

実際、使ってみたくなったでしょうか?

仮想環境でデモを試す

まずはトライアル!

仮想環境を作ることで、ローカルの環境を汚さずに試すことができます。
以下のコマンドを順番に入力して下さい。

python3 -m venv .venv
source .venv/bin/activate
pip install gfzs==0.2.1

gfzs init
gfzs demo

こんな風に表示されたら正しく動いてます。

image

閉じるときは、Ctrl-c で閉じます。

気にったら pip install gfzs==0.2.1 で入れて使って貰えると幸いです。

googlerを使った例

googlerを使った例を試すには1つお約束事項があります。

googlerでは検索結果をデフォルトで10件までしか取得できませんが、オプションを渡す事で一応100件とか取得できるように作られています。

だがしかし...

????????????????????????????????????????????
100件とか1000件とか取得しようとするとGoogle側にIPをブロックされるかもしれない事を忘れないでください。(試してIPがブロックされても責任は負えません。)
????????????????????????????????????????????

alert_tape

これは動作確認の時、私がやらかしてしまった時のgooglerのレスポンスをご覧ください。

$ googler --json -n 1000 東京
[ERROR] Connection blocked due to unusual activity. THIS IS NOT A BUG, please do NOT report it as a bug unless you have specific information that may lead to the development of a workaround. You IP address is temporarily or permanently blocked by Google and requires reCAPTCHA-solving to use the service, which googler is not capable of. Possible causes include issuing too many queries in a short time frame, or operating from a shared / low reputation IP with a history of abuse. Please do NOT use googler for automated scraping.

[エラー]異常なアクティビティが原因で接続がブロックされました。これはバグではありません。回避策の開発につながる可能性のある特定の情報がない限り、バグとして報告しないでください。お客様のIPアドレスはGoogleによって一時的または永続的にブロックされており、サービスを使用するにはreCAPTCHAを解決する必要がありますが、Googlerではできません。考えられる原因には、短期間で発行するクエリが多すぎる、または悪用の履歴がある共有/レピュテーションの低いIPから操作することが含まれます。自動スクレイピングにグーグルを使用しないでください。

という感じでIPがブロックされました。「あぁ... もう一生使えないかも...」って思ったのですが、2〜3日おいたら使えるようになりました。内心良かったーって思いました。?

という事で...

???????????????????????????????????????????
googlerがデフォルトで取得してくる件数をオプションでいじれなくした状態で使う事を激しく推奨します。
???????????????????????????????????????????

alert_tape

私はこんな感じでaliasを貼って使ってます。

function gfzs_google_fuzzy_search(){
  # 結果を取得しすぎてロボット判定されないようにgooglerの-nと--countオプションを渡せなくする
  FLAG_N=0
  FLAG_S=0

  GFZS_SCORE=30
  googler_input=()

  for OPT in "$@"
  do
      case $OPT in
          -n | --count)
              FLAG_N=1
              ;;
          -s | --score)
              FLAG_S=1
              GFZS_SCORE=$2
              shift
              ;;
          *)
              googler_input+=($1)
              ;;
      esac

      if [ $# -ne 0 ]; then shift; fi
  done

  echo $googler_input

  if [ "$FLAG_N" -eq 1 ]; then
    echo "Can't use -n(--count) option"
  else
    local res

    if [ "$FLAG_S" -eq 1 ]; then
      res=$(googler --json $googler_input | gfzs -s $GFZS_SCORE)
    else
      res=$(googler --json $googler_input | gfzs)
    fi

    if [ "$res" != "" ]; then
      echo $res
    fi
  fi
}
alias ggr=gfzs_google_fuzzy_search

使う時はこんな感じです。
typoしてても大丈夫です。googlerが勝手にそこらへん良しなにしてくれます。

ggr -s 30 python machne lerning

Ctrl-C を押して閉じてみましょう。

$ ggr python machne lerning
** Showing results for python machine learning; use -x, --exact for an exact search.

typoしてたから、「python machine learning」で探してみたよって言ってますね。
素晴らしい!

コマンドに関して

使えるコマンドは gfzs -h で確認できます。
ここまでで二つほど使ってないコマンドがあります。

  • gfzs edit
  • gfzs valid

edit

edit コマンドは、初期化の際、作られた設定ファイル(~/.gfzrc)を編集するために使われるコマンドです。EDITOR という環境変数にエディタを開くためのコマンドを設定して下さい。

例)

export EDITOR=code  # vscodeで開く場合
gfzs edit

詳しい設定に関してはこちらで確認して下さい。

valid

valid コマンドは、~/.gfzrc が有効な設定ファイルかどうかを調べるためのコマンドです。

# 有効な場合
$ gfzs valid
Config is valid.

無効な場合を試すために、gfzs edit で設定ファイルを以下のように修正してみて下さい。


コピペ用

{
"view": {
"hoge": {},
"fuga": [],
"footer": {
"messageaaa": "QUERY>",
"color": {
"message": {
"text": 8,
"background": 0,
"style": "normalaaaaa"
},
"hline": {
"text": 7,
"background": 0,
"styleaaaaaaaa": "linkbbbbbbbb"
}
}
},
"header": {
"color": {
"hline": {
"text": 7,
"background": 0,
"style": "normal"
}
}
},
"search_result": {
"color": {
"index": {
"text": 6,
"background": 0,
"style": "normal"
},
"title": {
"text": 2,
"background": 0,
"style": "bold"
},
"url": {
"text": 3,
"background": 0,
"style": "link"
},
"abstract": {
"text": 7,
"background": 0,
"style": "normal"
},
"markup_partial": {
"text": 2,
"background": 5,
"style": "normal"
},
"markup_char": {
"text": 1,
"background": 0,
"style": "normal"
}
}
},
"paging": {
"color": {
"common": {
"text": 2,
"background": 0,
"style": "bold"
}
}
}
}
}

この状態でコマンドを試すと以下のように表示されどこがおかしい設定かあたりをつけれるようになってます。

$ gfzs valid
Config is invalid.
Error: Contains unsupported key.   (key_path, value) = (view.hoge, {}).
Error: Contains unsupported key.   (key_path, value) = (view.fuga, []).
Error: Contains unsupported key.   (key_path, value) = (view.footer.messageaaa, QUERY>).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.text, 8).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.style, normalaaaaa).
Error: Contains unsupported key.   (key_path, value) = (view.footer.color.hline.styleaaaaaaaa, linkbbbbbbbb).

設定がおかしい状態で、demo コマンドを試してみましょう。

$ gfzs demo
Config is invalid.
Error: Contains unsupported key.   (key_path, value) = (view.hoge, {}).
Error: Contains unsupported key.   (key_path, value) = (view.fuga, []).
Error: Contains unsupported key.   (key_path, value) = (view.footer.messageaaa, QUERY>).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.text, 8).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.style, normalaaaaa).
Error: Contains unsupported key.   (key_path, value) = (view.footer.color.hline.styleaaaaaaaa, linkbbbbbbbb).

こんな風に設定がおかしいと起動しないようになってます。

実行時オプションに関して

キーワードでの絞りこみには、fuzzywuzzy を使って実装しており、このライブラリーが返してくるスコア〇〇以上の結果だけ画面に表示するための -s(--score) オプション(デフォルト値は30)が用意されています。

実際に使ってみたらよくわかります。

スコアを上げすぎると全く検索に引っかからなくなります。

gfzs -s 90 demo

image

逆に下げすぎると全く絞れなくなります。

gfzs -s 5 demo

image

なんか欲しいオプションあったらIssueを英語で起案して下さい。

まとめ

Googleの規制が緩くなって、一気に100件くらい取得できるようになったら本当に面白いツールになるかなって思ってます。(望み薄)

簡単に使えるので使ってみて下さい。(^ ^)

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

Googleの検索結果をpecoりたい「Google Fuzzy Search」を作った

はじめに

peco, percol, fzf, skimとかいろんなfuzzy findツールがある中、 googleの検索結果だけfuzzy findするツールがなかったので作ってみました。

Google Fuzzy Search とは

gfzs

とってもシンプルなツールで 「標準入力から受けたjsonをインクリメンタルサーチして、選択したアイテムをブラウザで開くためのツールです。jsonはgooglerで用意したものを使います。」

百聞は一見にしかずということで動いているものをご覧ください。(拡大してご覧ください。)

実行しているコマンドはこれです。

cat fixtures/tokyo.json | gfzs

gfzs

fixtures/tokyo.json東京 をキーワードにgooglerで調べたものを保存しておいて、それを標準入力として渡して使ってます。 QUERY> となっている部分に絞りたい単語 コロナ を入れて検索結果を絞り、Enter を押してブラウザで開いている様子です。

実際、使ってみたくなったでしょうか?

仮想環境でデモを試す

まずはトライアル!

仮想環境を作ることで、ローカルの環境を汚さずに試すことができます。
以下のコマンドを順番に入力して下さい。

python3 -m venv .venv
source .venv/bin/activate
pip install gfzs==0.2.1

gfzs init
gfzs demo

こんな風に表示されたら正しく動いてます。

image

閉じるときは、Ctrl-c で閉じます。

気にったら pip install gfzs==0.2.1 で入れて使って貰えると幸いです。

googlerを使った例

googlerを使った例を試すには1つお約束事項があります。

googlerでは検索結果をデフォルトで10件までしか取得できませんが、オプションを渡す事で一応100件とか取得できるように作られています。

だがしかし...

????????????????????????????????????????????
100件とか1000件とか取得しようとするとGoogle側にIPをブロックされるかもしれない事を忘れないでください。(試してIPがブロックされても責任は負えません。)
????????????????????????????????????????????

alert_tape

これは動作確認の時、私がやらかしてしまった時のgooglerのレスポンスをご覧ください。

$ googler --json -n 1000 東京
[ERROR] Connection blocked due to unusual activity. THIS IS NOT A BUG, please do NOT report it as a bug unless you have specific information that may lead to the development of a workaround. You IP address is temporarily or permanently blocked by Google and requires reCAPTCHA-solving to use the service, which googler is not capable of. Possible causes include issuing too many queries in a short time frame, or operating from a shared / low reputation IP with a history of abuse. Please do NOT use googler for automated scraping.

[エラー]異常なアクティビティが原因で接続がブロックされました。これはバグではありません。回避策の開発につながる可能性のある特定の情報がない限り、バグとして報告しないでください。お客様のIPアドレスはGoogleによって一時的または永続的にブロックされており、サービスを使用するにはreCAPTCHAを解決する必要がありますが、Googlerではできません。考えられる原因には、短期間で発行するクエリが多すぎる、または悪用の履歴がある共有/レピュテーションの低いIPから操作することが含まれます。自動スクレイピングにグーグルを使用しないでください。

という感じでIPがブロックされました。「あぁ... もう一生使えないかも...」って思ったのですが、2〜3日おいたら使えるようになりました。内心良かったーって思いました。?

という事で...

???????????????????????????????????????????
googlerがデフォルトで取得してくる件数をオプションでいじれなくした状態で使う事を激しく推奨します。
???????????????????????????????????????????

alert_tape

私はこんな感じでaliasを貼って使ってます。

function gfzs_google_fuzzy_search(){
  # 結果を取得しすぎてロボット判定されないようにgooglerの-nと--countオプションを渡せなくする
  FLAG_N=0
  FLAG_S=0

  GFZS_SCORE=30
  googler_input=()

  for OPT in "$@"
  do
      case $OPT in
          -n | --count)
              FLAG_N=1
              ;;
          -s | --score)
              FLAG_S=1
              GFZS_SCORE=$2
              shift
              ;;
          *)
              googler_input+=($1)
              ;;
      esac

      if [ $# -ne 0 ]; then shift; fi
  done

  if [ "$FLAG_N" -eq 1 ]; then
    echo "Can't use -n(--count) option"
  else
    local res

    if [ "$FLAG_S" -eq 1 ]; then
      res=$(googler --json $googler_input | gfzs -s $GFZS_SCORE)
    else
      res=$(googler --json $googler_input | gfzs)
    fi

    if [ "$res" != "" ]; then
      echo $res
    fi
  fi
}
alias ggr=gfzs_google_fuzzy_search

# 自分の場合
function ggr(){
  command=$1
  shift
  case $command in
    "abema")
      googler_input=($@)
      googler_input+=(-w abema.tv)
      gfzs_google_fuzzy_search $googler_input
      ;;
    "hulu")
      googler_input=($@)
      googler_input+=(-w hulu.jp)
      gfzs_google_fuzzy_search $googler_input
      ;;
    "amazon")
      googler_input=($@)
      googler_input+=(-w amazon.co.jp)
      gfzs_google_fuzzy_search $googler_input
      ;;
    "zenn")
      googler_input=($@)
      googler_input+=(-w zenn.dev)
      gfzs_google_fuzzy_search $googler_input
      ;;
    "qiita")
      googler_input=($@)
      googler_input+=(-w qiita.com)
      gfzs_google_fuzzy_search $googler_input
      ;;
    *)
      gfzs_google_fuzzy_search
      ;;
  esac
}

使う時はこんな感じです。
typoしてても大丈夫です。googlerが勝手にそこらへん良しなにしてくれます。

ggr -s 30 python machne lerning

Ctrl-C を押して閉じてみましょう。

$ ggr python machne lerning
** Showing results for python machine learning; use -x, --exact for an exact search.

typoしてたから、「python machine learning」で探してみたよって言ってますね。
素晴らしい!

コマンドに関して

使えるコマンドは gfzs -h で確認できます。
ここまでで二つほど使ってないコマンドがあります。

  • gfzs edit
  • gfzs valid

edit

edit コマンドは、初期化の際、作られた設定ファイル(~/.gfzrc)を編集するために使われるコマンドです。EDITOR という環境変数にエディタを開くためのコマンドを設定して下さい。

例)

export EDITOR=code  # vscodeで開く場合
gfzs edit

詳しい設定に関してはこちらで確認して下さい。

valid

valid コマンドは、~/.gfzrc が有効な設定ファイルかどうかを調べるためのコマンドです。

# 有効な場合
$ gfzs valid
Config is valid.

無効な場合を試すために、gfzs edit で設定ファイルを以下のように修正してみて下さい。


コピペ用

{
"view": {
"hoge": {},
"fuga": [],
"footer": {
"messageaaa": "QUERY>",
"color": {
"message": {
"text": 8,
"background": 0,
"style": "normalaaaaa"
},
"hline": {
"text": 7,
"background": 0,
"styleaaaaaaaa": "linkbbbbbbbb"
}
}
},
"header": {
"color": {
"hline": {
"text": 7,
"background": 0,
"style": "normal"
}
}
},
"search_result": {
"color": {
"index": {
"text": 6,
"background": 0,
"style": "normal"
},
"title": {
"text": 2,
"background": 0,
"style": "bold"
},
"url": {
"text": 3,
"background": 0,
"style": "link"
},
"abstract": {
"text": 7,
"background": 0,
"style": "normal"
},
"markup_partial": {
"text": 2,
"background": 5,
"style": "normal"
},
"markup_char": {
"text": 1,
"background": 0,
"style": "normal"
}
}
},
"paging": {
"color": {
"common": {
"text": 2,
"background": 0,
"style": "bold"
}
}
}
}
}

この状態でコマンドを試すと以下のように表示されどこがおかしい設定かあたりをつけれるようになってます。

$ gfzs valid
Config is invalid.
Error: Contains unsupported key.   (key_path, value) = (view.hoge, {}).
Error: Contains unsupported key.   (key_path, value) = (view.fuga, []).
Error: Contains unsupported key.   (key_path, value) = (view.footer.messageaaa, QUERY>).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.text, 8).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.style, normalaaaaa).
Error: Contains unsupported key.   (key_path, value) = (view.footer.color.hline.styleaaaaaaaa, linkbbbbbbbb).

設定がおかしい状態で、demo コマンドを試してみましょう。

$ gfzs demo
Config is invalid.
Error: Contains unsupported key.   (key_path, value) = (view.hoge, {}).
Error: Contains unsupported key.   (key_path, value) = (view.fuga, []).
Error: Contains unsupported key.   (key_path, value) = (view.footer.messageaaa, QUERY>).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.text, 8).
Error: Contains unsupported value. (key_path, value) = (view.footer.color.message.style, normalaaaaa).
Error: Contains unsupported key.   (key_path, value) = (view.footer.color.hline.styleaaaaaaaa, linkbbbbbbbb).

こんな風に設定がおかしいと起動しないようになってます。

実行時オプションに関して

キーワードでの絞りこみには、fuzzywuzzy を使って実装しており、このライブラリーが返してくるスコア〇〇以上の結果だけ画面に表示するための -s(--score) オプション(デフォルト値は30)が用意されています。

実際に使ってみたらよくわかります。

スコアを上げすぎると全く検索に引っかからなくなります。

gfzs -s 90 demo

image

逆に下げすぎると全く絞れなくなります。

gfzs -s 5 demo

image

なんか欲しいオプションあったらIssueを英語で起案して下さい。

まとめ

Googleの規制が緩くなって、一気に100件くらい取得できるようになったら本当に面白いツールになるかなって思ってます。(望み薄)

簡単に使えるので使ってみて下さい。(^ ^)

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

Atcoder Beginner Contest 163 躓いたところまとめ

Atcoderについて

AtCoder(アットコーダー)とは、「競技プログラミング」と呼ばれるコンピュータプログラムのコンテストを行うサービス、および高橋直大が代表を務めるその運営会社を指す。(Wikipediaより)
今回プログラミングの練習でいい機会と思い登録してみました。163回目のコンテストで躓いたところについてまとめていきます。

環境

Windows10
python 3.8.2

問題A

円周率について

いつも忘れてしまうのが円周率。表記方法がいくつかあります。

mathモジュールを使用する方法

math
import math

print(math.pi)

numpyを使用する方法

nupy
import numpy as np

print(np.pi)

mpmathを使用する方法

mpmath
from mpmath import *

mp.dps = 10
print(pi)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのinput関数

Pythonについて

Pythonは、汎用のプログラミング言語である。コードがシンプルで扱いやすく設計されており、C言語などに比べて、さまざまなプログラムを分かりやすく、少ないコード行数で書けるといった特徴がある。(Wikipediaより)
簡単に言うと有名で簡単なプログラミング言語です。

Pythonのinput関数

ちょっと躓いたことのリマインドとしてinput関数を挙げておきます。
input関数は、terminal等にキーボードで打ち込んだ文字を認識するための関数です。

python2系

python2
raw_input()

python3系

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

ディープラーニングまとめ(これから学習する人向け)

はじめに

今回はディープラーニングの基本をまとめました。
※これからディープラーニングを学習する人向けの記事です。

ニューラルネットワークについて

まずはディープラーニングの前身となるニューラルネットワークについて。

ニューラルネットワークとは、簡単にいうと人間の脳の中をマネしたアルゴリズム。
脳の中にはニューロンと呼ばれる神経細胞が何十億個とあり、それらが互いに結びつくことで神経回路という巨大なネットワークのようなものを作り上げている。

76238686.jpg

一つの情報が脳内に入ってくると、ニューロンに電気信号で伝わる。この電気信号が脳内の神経回路を駆け巡り、脳内のどの部分にどれだけの電気信号を伝えるかによって、情報を処理している。

この神経回路の仕組みを再現すれば、最高の機械学習ができるのではないか?
と考えて生まれたのがニューラルネットワークです。

単純パーセプトロン

単純なニューラルネットワークのモデル。

※参考図
Qiita用-1.jpg

複数の入力を受け取り、一つの出力を行う。
入力を受け取る部分を入力層、出力する部分を出力層と言う。
入力層と出力層とのつながりは「重み」というもので調整され、どれだけの特徴を出力層に伝えるかを調整している。

※入力層と出力層のみでは回路が単純すぎるため、線形分類しか行うことしかできない。

多層パーセプトロン

単純パーセプトロンの線形分類しかできないという弱点を解決するために、入力層と出力層の間に更に層を追加するというアプローチをとったものを多層パーセプトロンと言います。

※参考図

Qiita用-2.jpg

入力層と出力層の間の層のことを中間層(隠れ層)と言います。層が追加させただけで、単純パーセプトロンと仕組みは変わりません。
しかし、層が追加されたことでネットワーク全体の表現が向上し、非線形分類も行うことができるようになりました。

ディープラーニング

ディープラーニングとは、多層パーセプトロンの中間層(隠れ層)をより増やしたモデルになります。(一般的に中間層2つ以上)

※層が「深い」から、ディープラーニング(深層学習)と言われている。

ディープラーニングの学習の流れ

図で表すと以下のとおり
Qiita用-2.jpg

①順伝播
与えられた入力を重みで調整し、予測値を出力する。
(一般的に初期値の重みはランダムで設定される)

②誤差の計算(損失関数)
予測値と教師データである正解値の誤差を計算する。

具体的な計算方法は以下のとおり。
回帰:平均2乗誤差
分類:交差エントロピー誤差

③逆伝播(誤差逆伝播法)
損失関数(誤差)を最小化するために逆向きに学習し、パラメーター(重み等)を更新する。

④ ①〜③を繰り返すことでディープラーニングの予測精度を高めていきます。

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

損失関数(誤差)を最小化するために逆向きに学習し、パラメーター(重み等)を更新する作業。

パラメーターの更新の際に重要なことは大きく2つ!
○パラメーターを増やすか減らすか決める。
(勾配降下法)
○どれくらい増減させるか
(学習率)

※ディープラーニングの実装の際にはオプティマイザー(optimizer)と言われる。
ADam,RMSPropなどが使われる。

ディープラーニングの特徴まとめ

メリット
・予測精度が圧倒的に高い
・パラメーターの数が多い
・特徴エンジニアリングが不要(学習に影響を与えそうなデータを抽出する作業)

デメリット
・単純な問題の場合は単純なモデル(重回帰分析)の方が性能が良い
・計算コストが非常に高い
・大量の学習データが必要

おわりに

以上です。
ディープラーニングの基本をまとめました。

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

macでpythonの仮想環境作成

macでpythonの仮想環境を構築する

前提

・macOSでpythonの仮想環境を構築する
・今までanacondaで仮想環境を作っていたが、ふとターミナルで仮想環境を作るには?と思い調べた

手順

1.仮想環境の作成

1-1.仮想環境を作りたいディレクトリまで移動
お馴染みのcd
1-2.ターミナルに以下のコードを入力

python3 -m venv 環境名(例:.venv)

2.仮想環境の起動

. .venv/bin/activate

3.終了

deactivate

最後に

今までpythonの実行をanacondaのspyderというIDEを使っていたため、急にターミナルやVScodeで実行するのに少し抵抗があったが、少しずつ慣れていきたい

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

2次元CNNによるBitcoin価格予測

個別銘柄のBitcoinを対象として,2次元CNNを用いた予測を行いました.

5分先の価格の騰落を予測する,2値分類問題としました.

1. データセット

candlestickchart.png

入力データは,上記のようなローソク足チャート画像としました.

2016年1月1日~2016年3月31日までのBitcoin/USD価格,1分足の131,040件を取得しました.

1枚の画像に30件のデータを用いた為,4368枚の画像が生成されました.

前半の3968枚を訓練データとして分割し,後半の400枚をテストデータとして分割しました.

Google Colaboratoryへ全ての訓練データをアップロードすることができなかった為,前半の2800枚を訓練データとしました.

訓練データにはデータ拡張を行い,最終的には11,200枚の訓練データと400枚のテストデータを用いました.

2. 2次元CNNアーキテクチャ

(Conv2D relu + MaxPooling2D)2層 + Flatten + Dropout(0.3) + Dense relu + Dense sigmoid

エポック数は,200としました.

また,Dropoutを低い割合で組み込むことで,精度の向上が見られました.

3. 結果

最終的なtest accuracyは,0.575となりました.

学習データを増やすことで,精度の向上を図ることができる可能性があります.

4. 参考文献

  1. Ashwin Siripurapu: Convolutional Networks for Stock Trading.

  2. Rosdyana Mangir Irawan Kusuma, Trang-Thi Ho, Wei-Chun Kao, Yu-Yen Ou, Kai-Lung Hua: Using Deep Learning Neural Networks and Candlestick Chart Representation to Predict Stock Market, arXiv, 2019.

  3. 池田欽一: 株価ローソク足チャート画像を用いた畳み込みニューラルネットワークによる株価変動予測, 北九州市立大学「商経論集」第54巻第1・2・3・4巻合併号, 2019.

  4. 白方健司,津田博史: 畳み込みニューラルネットワークによる株価インデックス騰落予測, THE HARRIS SCIENCE REVIEW OF DOSHISHA UNIVERSITY, VOL.59, NO.2 July 2018.

  5. 宮崎邦洋,松尾豊: 深層学習を用いた株価予測の分析,一般社団法人 人工知能学会,2017.

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

TweepyでPINベース認証する方法

TweepyでPINベース認証をやったのでメモ程度に

前提条件

Python 3.7.4
tweepy 3.10.0

PINベース認証する方法

tewwpy_auth.py
import tweepy
import webbrowser

TWITTER_CONSUMER_KEY = 'xxxxxxxxxxxxxxxxxxxxxx'
TWITTER_CONSUMER_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxx'

# ここで oob と入力することで、アクセス許可後にPINが表示される様になる
auth = tweepy.OAuthHandler(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, 'oob')

# 認証用URLを取得後ブラウザで開く
authorization_url= auth.get_authorization_url()
webbrowser.open(authorization_url)

# アクセスを許可後出てきたPINを入力
print('PIN CODE >>', end='')
pin_code = input()

# PINコードを元にトークンなどを取得
auth.get_access_token(pin_code)
print(f"ACCESS_TOKEN = {auth.access_token}")
print(f"ACCESS_SECRET = {auth.access_token_secret}")

# 取得した情報を認証情報に追加
auth.set_access_token(auth.access_token, auth.access_token_secret)
api = tweepy.API(auth)
username = api.me().name
print(f"ユーザー名: {username}")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

herokuを使ってpythonのflaskアプリをデプロイ

概要

FlaskでWebアプリを作り、herokuを使って公開します。
今回は世の中でもっとも簡単なアプリケーションである、Hello worldを表示するアプリケーションを作ります。
そのため、HTMLとflaskの連携等のお話はしません。

pythonのコード

flaskをインストールしていない場合はpipでインストールします。

pip install flask

pythonのコードは以下です。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return ('Hello world')

if __name__ == '__main__':
    app.run()

これをapp.pyとして保存します。
簡単にコードの説明を。

まずは、Flaskを使ったアプリ名をappとします。
('/')はホーム画面的な意味で、このURLにindexという関数を適用しますよーということになります。

Hello worldを返り値とするわけです。

最後のif文はおまじないみたいなもので、基本的にmainという名前のものが実行されるのですが、このmainがnameならアプリを実行するという意味です。

最初にFlask(name)としていますから、mainと一致して、アプリが起動するわけです。

デプロイの準備

gunicornのインストール

herokuを使ってデプロイする際に必要となるものです。

pip install gunicorn

requirements.txtの作成

デプロイする側に、このアプリはこんなライブラリやモジュールを使っていますよーと教えてあげる必要があります。

pip freeze > requirements.txt

これで作成されます。ローカルでこのコマンドを実行すると、ローカルに入っているモジュールが全て吐き出されるので、必要なもののみを残せば良いです。消さないとエラーの原因にもなりますし、何よりアプリが重くなります。(pandasやnumpyはかなり重いので、使わないならない方がいい)
今回はflaskとgunicornのみですね。

heroku cliのインストール

コマンドラインを使ってデプロイする際に必要となるものです。
GoogleでHeroku cliと検索すればいいです。一応URL貼っておきます。
https://devcenter.heroku.com/ja/articles/heroku-cli#download-and-install

MACならHomebrewでインストールした方が楽ですね。

brew tap heroku/brew && brew install heroku

Procfileの作成

app.pyと同じ階層にProcfileという名前のファイルを作成し、以下の内容を書いて保存します。

web: gunicorn app:app --log-file=-

デプロイ

まずはGitレポジトリの準備をしましょう。ここではGitの細かい説明はしません。

git init
git add .
git commit -m "first commit"

herokuにアクセスしましょう。以下の作業はheroku cliがダウンロードできていないとできません。確認したいなら

heroku -v

を実行してバージョンが返ってくればOKです。

heroku login

なんかでてきますが、構わずEnterで問題ありません。するとブラウザーが立ち上がって、Herokuのログイン画面が出てきます。登録していない人はSign upしたあと、再度heroku login を実行します。

ブラウザーでLoginボタンを押してログインできたら、ブラウザーは消して結構です。

次のステップでアプリケーションのURLを決めます。

heroku create test-test-test

今回はtest-test-testというURLにしました。他の人と重複していたり、URLとして使用できない文字があるとエラーになります。
ちなみにアンダーバー _ は使用できないっぽいです。

あともう少し。
一応プッシュ先の確認をしましょう

git remote -v

先ほどcreateで作成したURLが返ってくると思います。そうなれば最後のステップ...

git push heroku master

これでアプリケーションのデプロイは完了です。ブラウザーでURLを叩くか、

heroku open

で確認できます。

ファイルを変更した場合

app.pyを変更した場合は以下のコマンドでOKです。

git add app.py
git commit -m "change app.py"
git remote -v
git push heroku master 

以上。

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

CRFモデルで時系列データからある時点の状態を推定する

これを読んで得られること

  • CRF(条件付き確率場)を用いた時系列データの分類
    • 自分のためにまとめておこうというモチベーション
    • まとめているうちに自分の中で整理されてきて、記事にするほどじゃないよねという気になっている

これを読んでも書いていないこと

  • 自然言語のCRFによる分類の方法
    • 各ライブラリにあるチュートリアルが詳しいのでそちらへ
  • CRFの詳細説明
    • これも大学の講義資料とか書籍とかたくさん詳しい方がいらっしゃるのでそちらへ...

CRFとは

深層学習がはやる前に流行っていた教師あり学習のひとつ。例えば、文章中の固有表現の抽出といった系列に対するラベルの付与(系列ラベリング)問題を解く際に適用される。

系列ラベリング

  • 系列ラベリング
    • 系列に対してラベルを付与するもの
      • 隠れマルコフモデル(HMM)やCRFや深層学習のLSTMがある
  • 系列データ
    • 自然言語処理や時系列データ(前後関係がある)
    • 画像(周辺情報も大事)
  • ラベリング
    • 自然言語処理は分類で何かを分類すること(MeCabで形態素解析するとか)
    • 時系列データの場合は回帰で今の時点から次の時点を予測すること
    • 画像のピクセル毎の分類(セマンティックセグメンテーション)

今回やりたいことは、時系列データを用いてある時点での挙動を分類すること。
例えば、人の速度と歩数から歩いているか、走っているか、自転車に乗っているかといった推定をしたり、心臓の周波数データから緊張しているとかリラックスしているとかを推定すること。

HMMやCRFの関係性をまとめると次のような図となる。(日本語を付した。一部意訳。)
relationship_diagram_HMM_CRF.png
(引用: An Introduction to Conditional Random Fields https://arxiv.org/abs/1011.4088

CRFとは

  • マルコフ確率場の一種
    • マルコフ確率場とは依存関係を無向グラフで表すもの(上図の下段)
  • 学習で最大化する対象
    • 設計する事後確率
  • 教師あり学習

実装手順

ライブラリ候補

  • PyStruct
  • CRFSuite
  • sklearn_crfsuite

sklearn_crfsuiteはCRFSuiteのラッパーでsklearnのモデルセレクションのパッケージが利用できるようになっている。つまりはハイパーパラメーターチューニングが楽。なので今回はsklearn-crfsuiteを用いる。ちなみに線形連鎖条件付き確率場を実現しているライブラリである。

実装ステップ

  1. データを集める
  2. データを学習データとテストデータに分割する train_test_split
  3. 学習データとテストデータを整形する
  4. 系列データに分割する for文を用いて切ったりくっつけたり
  5. CRFのモデルを決めるsklearn_crfsuite.CRF()で最適化アルゴリズムを設定したり
  6. 学習データを学習させる crf.fit(X_train, y_train)
  7. テストデータで推論する crf.predict(X_test)
  8. テストデータの正解率を確認する metrics.flat_classification_report() recallとかもこれ
  9. ハイパーパラメータのチューニング RandomizedSearchCV
  10. ハイパーパラメーターと正解のスコアのばらつきを可視化

手順は公式チュートリアルを見ながら時系列データに応用
https://sklearn-crfsuite.readthedocs.io/en/latest/tutorial.html

1. データを集める

今回は気象庁の気温と気圧から天気を予測する。
2020年1月1日から2021年1月22日までの3時間毎の気温と気圧と天気をお借りした。
https://www.data.jma.go.jp/gmd/risk/obsdl/index.php

df = pd.read_csv('202001data.csv', encoding='shift-jis', skiprows=3)
df = df.append(pd.read_csv('202004data.csv', encoding='shift-jis', skiprows=3))
df = df.append(pd.read_csv('202007data.csv', encoding='shift-jis', skiprows=3))
df = df.append(pd.read_csv('202010data.csv', encoding='shift-jis', skiprows=3))
df.columns = ['datetime', 'temparature', 'weather', 'pressure']
df = df[['datetime', 'temparature', 'pressure', 'weather']]
df = df.dropna(how='any')
df = df.reset_index(drop = True)

データの外観

2. データを学習データとテストデータに分割する

X_df = df[['temparature', 'pressure']]
y_s = df['weather']
X_train, X_test, y_train, y_test = train_test_split(X_df, y_s, random_state=0)

3. 学習データとテストデータを整形する

入力データはdictionary型にする。

X_train = X_train.to_dict(orient='records')
X_test = X_test.to_dict(orient='records')

正解ラベルはリスト型

y_train = y_train.to_list()
y_test = y_test.to_list()

4. 系列データに分割する

全データを等間隔のwindow幅を持つ系列データに整形する。
これは辞書型の集合の配列とする。
学習データとテストデータは[[{}{}{}], [{}{}{}], [{}{}{}]]こんなイメージ。
正解ラベルは[[], [], []]こんなイメージ。

from_i = 0
step = 10 # window幅
to_i = len(X_train) - step

X_trains = []
for i in range(from_i, to_i, step):
    X_trains.append(X_train[i:i+step])

y_trains = []
for i in range(from_i, to_i, step):
    y_trains.append(y_train[i:i+step])

to_i = len(X_test) - step
X_tests = []
for i in range(from_i, to_i, step):
    X_tests.append(X_test[i:i+step])

y_tests = []
for i in range(from_i, to_i, step):
    y_tests.append(y_test[i:i+step])

5. CRFのモデルを決める

crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs',
    c1=0.1,
    c2=0.1,
    max_iterations=100,
    all_possible_transitions=True
)

6. 学習データを学習させる

crf.fit(X_trains, y_trains)

7. テストデータで推論する

labels = list(crf.classes_)
y_pred = crf.predict(X_tests)
metrics.flat_f1_score(y_tests, y_pred, 
                      average='weighted', labels=labels)

8. テストデータの正解率を確認する

sorted_labels = sorted(
    labels, 
    key=lambda x: (x[1:], x[0])
)
print(metrics.flat_classification_report(
    y_tests, y_pred, labels=sorted_labels, digits=3
))

精度結果

9. ハイパーパラメータのチューニング

# define fixed parameters and parameters to search
crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs', 
    max_iterations=100, 
    all_possible_transitions=True
)
params_space = {
    'c1': scipy.stats.expon(scale=0.5),
    'c2': scipy.stats.expon(scale=0.05),
}

# use the same metric for evaluation
f1_scorer = make_scorer(metrics.flat_f1_score, 
                        average='weighted', labels=labels)

# search
rs = RandomizedSearchCV(crf, params_space, 
                        cv=2, 
                        verbose=1, 
                        n_jobs=-1, 
                        n_iter=50, 
                        scoring=f1_scorer)
rs.fit(X_trains, y_trains)

10. ハイパーパラメーターと正解のスコアのばらつきを可視化

_x = [s['c1'] for s in rs.cv_results_['params']]
_y = [s['c2'] for s in rs.cv_results_['params']]
_c = [s for s in rs.cv_results_['mean_test_score']]

fig = plt.figure()
fig.set_size_inches(6, 6)
ax = plt.gca()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_xlabel('C1')
ax.set_ylabel('C2')
ax.set_title("Randomized Hyperparameter Search CV Results (min={:0.3}, max={:0.3})".format(
    min(_c), max(_c)
))

ax.scatter(_x, _y, c=_c, s=60, alpha=0.9)

print("Dark blue => {:0.4}, dark red => {:0.4}".format(min(_c), max(_c)))

結果

結果より気温と気圧だけでは最大で28%の予測精度であった。
つまり気温と気圧からCRFで天候を予測することは難しい。
本記事の目的としては予測精度を上げることではなく、CRFで時系列データのある点の予測を行うことである。

何かありましたらコメントいただけると幸いです。

参考ページ

sklearn-crfsuite
https://sklearn-crfsuite.readthedocs.io/en/latest/index.html

Using CRF in Python
http://acepor.github.io/2017/03/06/CRF-Python/

An Introduction to Conditional Random Fields
https://arxiv.org/abs/1011.4088

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

csvファイルに書かれた語をgoogle翻訳に掛け、csv出力するプログラム

はじめに

前回(https://qiita.com/syunpeta/private/63d2de6faef992bbad39) に引き続きseleniumで遊んでみようということでcsvファイルに書かれた文または語句を自動的にgoogle翻訳に掛け、それを同じcsvファイルに出力するプログラムを作成しました。seleniumとChromeDriverの導入は前回を参考にしてください。

動作環境

Windows 10 Home (64bit)
python 3.8.7
ChromeDriver 87.0.4280.88
selenium 3.141.0

ソースコード

import csv
from pathlib import Path
from selenium import webdriver
import time

#翻訳して出力を配列に格納する関数
def translate(*tl):
    tmp=[]
    for i in range(len(tl)):
        #csv上から順に入力
        before_in.send_keys(tl[i])
        time.sleep(2)
        #出力部を取得
        after_out = driver.find_element_by_css_selector("span[jsname='W297wb']") 
        tmp.append(after_out.text)
        before_in.clear()
    return tmp



#csvファイルを開く
filepath = Path(r"翻訳したいcsvファイルのあるディレクトリの絶対パス")
csv_file = list(filepath.glob("*.csv"))
csv_file_op = csv_file.pop()

#csvファイルの一列目を2次元配列で読み込み
with open(csv_file_op,'r',encoding='utf-8-sig') as fp:
    translate_list = list(csv.reader(fp))
    print(translate_list)

#翻訳言語を選択
print("翻訳する前の言語を入力してください.(ex. 英語:en 日本語:ja 韓国語:ko 中国語:zh-CN")
before_lang = input()
print("翻訳した後の言語を入力してください.(ex. 英語:en 日本語:ja 韓国語:ko 中国語(簡体):zh-CN) 中国語(繁体):zh-TW")
after_lang = input()

#google翻訳をChromeで開く
driver = webdriver.Chrome(r"chromedriverの格納されているディレクトリの絶対パス/chromedriver.exe")
url = "https://translate.google.co.jp/?hl=ja&sl="+ before_lang +"&tl="+ after_lang +"&op=translate"
driver.get(url)

#2秒待機
time.sleep(2)

#入力部を取得
before_in = driver.find_element_by_xpath("//*[@id='yDmH0d']/c-wiz/div/div[2]/c-wiz/div[2]/c-wiz/div[1]/div[2]/div[2]/c-wiz[1]/span/span/div/textarea")

ans_arr = translate(*translate_list)

#csv書き込み
with open(csv_file_op, 'w', encoding= 'utf-8-sig', newline= "") as fp2:

    i=0
    #結果と翻訳前を結合
    for row in translate_list:
        row.append(ans_arr[i])
        i+=1
    writer = csv.writer(fp2)

    for k in translate_list:
        writer.writerow(k)

#ブラウザ閉じる        
driver.close()

部分的に解説

#csvファイルの一列目を2次元配列で読み込み
with open(csv_file_op,'r',encoding='utf-8-sig') as fp:
    translate_list = list(csv.reader(fp))
    print(translate_list)

with 構文を使ってファイルを開いています。encodingはutf-8-sigとして下さい。sigを付けた理由はBOMありのcsvファイルを読み込むためです。(参考資料[1]を参照してください)また、list(csv.reader(fp))でcsvファイルの中身を2次元配列で得ています。これは後に翻訳後の結果と結合して一行に書き込むためです。

#翻訳して出力を配列に格納する関数
def translate(*tl):
    tmp=[]
    for i in range(len(tl)):
        #csv上から順に入力
        before_in.send_keys(tl[i])
        time.sleep(2)
        #出力部を取得
        after_out = driver.find_element_by_css_selector("span[jsname='W297wb']") 
        tmp.append(after_out.text)
        before_in.clear()
    return tmp

翻訳する文字を入力し、翻訳結果を取得する関数です。翻訳したい文字列の配列を引数に渡して、forループで配列の長さ分だけ繰り返し翻訳をしています。google翻訳の翻訳結果の出力部はfind_element_by_css_selector("span[jsname='W297wb']")で取得しています。また、一回一回入力をクリアするためにclear()を実行しています。戻り値は出力結果を要素とした配列です。

with open(csv_file_op, 'w', encoding= 'utf-8-sig', newline= "") as fp2:

    i=0
    #結果と翻訳前を結合
    for row in translate_list:
        row.append(ans_arr[i])
        i+=1
    writer = csv.writer(fp2)

    for k in translate_list:
        writer.writerow(k)

書き込みのためにもう一度csvファイルをwith構文で開いています。この時、オプションでnewline=""とすることでcsvファイルの出力時に一行空くことを防いでいます。#以下のforループでは翻訳前と翻訳語の配列を結合しています。結合した配列を一行ずつ書き込むことで翻訳前と翻訳語を横並びで書き込めるようになっています。

csvファイルの書式

翻訳したい文字列を一列目に書き込んでください。
例:

翻訳したい文字列 空欄 空欄・・・
大学
東京
太陽
etc・・・

翻訳結果は次の列(2列目)に出力されます。

おまけ

個人的に自動入力されていく様子が面白いのでHeadlessモードは使いませんでしたが、使うと少し処理が早くなるかも?・・・

参考資料

以下のサイトを参考にさせていただきました。
[1]https://qiita.com/showmurai/items/60d32006d13512ffeaff
[2]https://note.nkmk.me/python-file-io-open-with/
[3]https://qiita.com/ryokurta256/items/defc553f5165c88eac95

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

flickr APIで富士山の画像を自動取得

Udemyで【画像判定AIアプリ開発・パート2】Django・TensorFlow・転移学習による高精度AIアプリ開発を受講した。オリジナルの機械学習プログラムを作る際に、インターネット上から画像を取得するのが非常に手間だと感じていたので、画像取得自動化の方法を記録しておく。

flicker APIのKey取得

flickerのホームページ
→ フッターのナビゲーションバーからDevelopersを選択
→ 画面上部からAPIを選択
→ Getting Startedの①にあるRequest an API keyを選択
→ ①のGet your API keyにあるRequest an API keyを選択
→ 商用利用ではないので、APPLY FOR A NON-COMMERCIAL KEYを選択
→ 作りたいアプリケーションの名前、アプリケーションの説明を入力
→ チェックボックスを埋めて、SUBMITを選択
→ KeyとSecretの取得完了!

flicker_key.png

画像をダウンロードするPythonファイル

これから目的の画像を取得する。

flickr APIのパッケージを仮想環境内にインストールする。私はAnaconda仮想環境を使っており、パッケージのインストールにはpip installではなくconda installを使うようにしているが、Anaconda Cloudで「flickrapi」で検索しても情報がほとんどなかったため、pip installを使った。(それっぽいパッケージもあるにはあるが、300ダウンロードにも満たないものだったので避けた。)

$ pip install flickrapi

今回はdownload.pyというファイルで富士山の画像のダウンロードを自動でおこなう。download.pyと同じディレクトリにdataというディレクトリを作成し、さらにその内部にmt.fujiというディレクトリを作成した。このmt.fujiの中に画像をダウンロードする。ソースコードは以下の通りである。

from flickrapi import FlickrAPI
from urllib.request import urlretrieve
import os, time, sys

key ="**************************"  # 先ほど取得したKeyをコピー&ペースト
secret = "****************"  # 先ほど取得したSecretをコピー&ペースト
wait_time = 1

keyword = sys.argv[1]
savedir = "./data/" + keyword

flickr = FlickrAPI(key, secret, format='parsed-json')
result = flickr.photos.search(
    text = keyword,
    per_page = 400,
    media = 'photos',
    sort = 'relevance',
    extras = 'url_q, license',
    )

photos = result['photos']

for i, photo in enumerate(photos['photo']):
    url_q = photo['url_q']
    filepath = savedir + '/' + photo['id'] + '.jpg'
    if os.path.exists(filepath):
        continue
    urlretrieve(url_q, filepath)
    time.sleep(wait_time)

このように書いたうえでターミナルで

$ python download.py mt.fuji

と入力すると画像のダウンロードが始まる。数分後、mt.fujiの中身を確認すると、以下のように画像がダウンロードされている。

image_download.png

これで、画像を手作業でダウンロードするという手間を大幅に省くことができた。しかしこのままでは関係ない画像も多く混じっているため、もっと工夫が必要である。

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

ゲシュタルトパターンマッチングを用いた文字列の類似度計算

はじめに

2つの文字列の類似度を計算するアルゴリズムの1つである、
ゲシュタルトパターンマッチング
について調べたので、内容をまとめました。

※本記事は、Wikipedia(英語版)の内容をベースとしています。

目次

実装

ゲシュタルトパターンマッチングは、Pythonのdifflibで以下のように実装できます。

import difflib

word = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6))
# ['apple', 'ape']

ある文字列(word = 'appel')と似ているものを、文字列のリスト(possibilities = ['ape', 'apple', 'peach', 'puppy'])のなかから取り出すことができます。

get_close_matchesの引数と戻り値の詳細は以下です。

  • 引数
    • word:マッチさせたい文字列
    • possibilities:wordにマッチさせる文字列のリスト
    • n(デフォルト3):マッチさせる文字列の最大数。1以上を指定。
    • cutoff(デフォルト0.6):wordとの一致率。0以上1以下を指定。
  • 戻り値
    • マッチした文字列のリスト。cutoff以上の一致率の文字列リストを、一致率の高い順(似ている順)にソートして返却。

マッチさせる文字列の数はn、マッチングの厳しさはcutoffで調整できます。

# n=1
print(difflib.get_close_matches(word, possibilities, n=1, cutoff=0.6))
# ['apple']

# cutoff=0.8
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.8))
# ['apple']

マッチの一致率は以下で調べることができます。cutoffの参考にすると良いかもしれません。

str1 = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
for str2 in possibilities:
  ratio = difflib.SequenceMatcher(None, str1, str2).ratio()
  print(str2, ratio)

# ape 0.75
# apple 0.8
# peach 0.4
# puppy 0.4

ゲシュタルトパターンマッチングとは

ゲシュタルトパターンマッチング(Gestalt Pattern Matching)』とは、2つの文字列の類似度を判定するためのアルゴリズムです。Ratcliff, Obershelpによって1983年に考案されました。Ratcliff/Obershelp Pattern Recognitionとも呼ばれているようです。
(以下、その頭文字をとって$D_{ro}$などと表記する)

「ゲシュタルト(Gestalt)」は、「形」「形態」「状態」といった意味を表すドイツ語です。

「ゲシュタルト崩壊」という言葉を聞いたことがある方も多いのではないでしょうか。ずっと同じ文字を眺めていると、それが何を表す文字なのかわからなくなってしまう感覚のことです。

ゲシュタルト崩壊(ゲシュタルトほうかい、独: Gestaltzerfall)とは、知覚における現象のひとつ。 全体性を持ったまとまりのある構造(Gestalt, 形態)から全体性が失われてしまい、個々の構成部分にバラバラに切り離して認識し直されてしまう現象をいう。幾何学図形、文字、顔など、視覚的なものがよく知られているが、聴覚や皮膚感覚、味覚、嗅覚においても生じうる。
Wikipediaより引用

「ゲシュタルト」のこれらの意味より、
「ゲシュタルトパターンマッチング」は、細かい部分は気にせず、あくまで文字列全体のまとまりに重きを置いて類似度を計算する手法
というのが個人的な印象です。

アルゴリズム

ゲシュタルトパターンマッチングによる、2つの文字列$S_1, S_2$の類似度$D_{ro}$は、以下で表されます。

D_{ro}(S_1, S_2)=\frac{2K_m}{|S_1|+|S_2|}

ここで

  • $K_m$:マッチした文字数
  • $|S_1|, |S_2|$:それぞれ$S_1, S_2$の文字列の長さ

であり、類似度$D_{ro}$の取り得る値の範囲は

0 \leq D_{ro} \leq 1

となります。
1に近いほど類似度は高く、0に近いほど類似度は低くなります。ちなみに、

  • $D_{ro}=1$:完全一致
  • $D_{ro}=0$:1文字もマッチしない

となります。

$S_1$ S I M I L A R I T Y
$S_2$ S I M I R E L I T Y

左から順に見ていくと、最長でSIMIの4文字がマッチしています。
残った文字列に対して再び見ていくと、最長でITYの3文字がマッチしています。

よって、この場合の類似度は以下になります。

\begin{align}
D_{ro}&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"SIMI"|+|"ITY"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(4+3)}{10+10}\\
&=\frac{7}{10}=0.7
\end{align}

性質

非可換性

ゲシュタルトパターンマッチングは非可換(non-commutative)、すなわち交換できません。

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

以下の例で確認してみましょう。
2つの文字列を

  • $S_1$:Gestalt Pattern Matching
  • $S_2$:Gestalt Practice

とすると、マッチする文字列は以下のように異なります。

  • $D_{ro}(S_1, S_2)$:Gestalt P, a, t, e
  • $D_{ro}(S_2, S_1)$:Gestalt P, r, a, c, i

よって、それぞれ類似度を計算すると

\begin{align}
D_{ro}(S_1, S_2)&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"a"|+|"t"|+|"e"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(9+1+1+1)}{24+16}\\
&=\frac{24}{40}=0.6\\
\\
D_{ro}(S_2, S_1)&=\frac{2K_m}{|S_2|+|S_1|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"r"|+|"a"|+|"c"|+|"i"|})}{|S_2|+|S_1|}\\
&=\frac{2\cdot(9+1+1+1+1)}{16+24}\\
&=\frac{26}{40}=0.65\\
\end{align}
\begin{align}
\end{align}

となり、確かに

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

と非可換となっています。

実際に、コードで確認すると

str1 = 'GESTALT PATTERN MATCHING'
str2 = 'GESTALT PRACTICE'
print(difflib.SequenceMatcher(None, str1, str2).ratio())
print(difflib.SequenceMatcher(None, str2, str1).ratio())
# 0.6
# 0.65

と同様の結果を得ることができます。

参考文献

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

ゲシュタルトパターンマッチングによる文字列類似度の計算

はじめに

2つの文字列の類似度を計算するアルゴリズムの1つである、
ゲシュタルトパターンマッチング
について調べたので、内容をまとめました。

※本記事は、Wikipedia(英語版)の内容をベースとしています。

目次

実装

ゲシュタルトパターンマッチングは、Pythonのdifflibで以下のように実装できます。
※環境は、Google Colaboratoryを使用

import difflib

word = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6))
# ['apple', 'ape']

ある文字列(word = 'appel')と似ているものを、文字列のリスト(possibilities = ['ape', 'apple', 'peach', 'puppy'])のなかから取り出すことができます。

get_close_matchesの引数と戻り値の詳細は以下です。

  • 引数
    • word:マッチさせたい文字列
    • possibilities:wordにマッチさせる文字列のリスト
    • n(デフォルト3):マッチさせる文字列の最大数。1以上を指定。
    • cutoff(デフォルト0.6):wordとの一致率。0以上1以下を指定。
  • 戻り値
    • マッチした文字列のリスト。cutoff以上の一致率の文字列リストを、一致率の高い順(似ている順)にソートして返却。

マッチさせる文字列の数はn、マッチングの厳しさはcutoffで調整できます。

# n=1
print(difflib.get_close_matches(word, possibilities, n=1, cutoff=0.6))
# ['apple']

# cutoff=0.8
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.8))
# ['apple']

マッチの一致率は以下で調べることができます。cutoffの参考にすると良いかもしれません。

str1 = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
for str2 in possibilities:
  ratio = difflib.SequenceMatcher(None, str1, str2).ratio()
  print(str2, ratio)

# ape 0.75
# apple 0.8
# peach 0.4
# puppy 0.4

ゲシュタルトパターンマッチングとは

ゲシュタルトパターンマッチング(Gestalt Pattern Matching)』とは、2つの文字列の類似度を判定するためのアルゴリズムです。Ratcliff, Obershelpによって1983年に考案されました。Ratcliff/Obershelp Pattern Recognitionとも呼ばれているようです。
(以下、その頭文字をとって$D_{ro}$などと表記する)

「ゲシュタルト(Gestalt)」は、「形」「形態」「状態」といった意味を表すドイツ語です。

「ゲシュタルト崩壊」という言葉を聞いたことがある方も多いのではないでしょうか。ずっと同じ文字を眺めていると、それが何を表す文字なのかわからなくなってしまう感覚のことです。

ゲシュタルト崩壊(ゲシュタルトほうかい、独: Gestaltzerfall)とは、知覚における現象のひとつ。 全体性を持ったまとまりのある構造(Gestalt, 形態)から全体性が失われてしまい、個々の構成部分にバラバラに切り離して認識し直されてしまう現象をいう。幾何学図形、文字、顔など、視覚的なものがよく知られているが、聴覚や皮膚感覚、味覚、嗅覚においても生じうる。
Wikipediaより引用

「ゲシュタルト」のこれらの意味より、
「ゲシュタルトパターンマッチング」は、細かい部分は気にせず、あくまで文字列全体のまとまりに重きを置いて類似度を計算する手法
というのが個人的な印象です。

アルゴリズム

ゲシュタルトパターンマッチングによる、2つの文字列$S_1, S_2$の類似度$D_{ro}$は、以下で表されます。

D_{ro}(S_1, S_2)=\frac{2K_m}{|S_1|+|S_2|}

ここで

  • $K_m$:マッチした文字数
  • $|S_1|, |S_2|$:それぞれ$S_1, S_2$の文字列の長さ

であり、類似度$D_{ro}$の取り得る値の範囲は

0 \leq D_{ro} \leq 1

となります。
1に近いほど類似度は高く、0に近いほど類似度は低くなります。ちなみに、

  • $D_{ro}=1$:完全一致
  • $D_{ro}=0$:1文字もマッチしない

となります。

$S_1$ S I M I L A R I T Y
$S_2$ S I M I R E L I T Y

左から順に見ていくと、最長でSIMIの4文字がマッチしています。
残った文字列に対して再び見ていくと、最長でITYの3文字がマッチしています。

よって、この場合の類似度は以下になります。

\begin{align}
D_{ro}&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"SIMI"|+|"ITY"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(4+3)}{10+10}\\
&=\frac{7}{10}=0.7
\end{align}

性質

非可換性

ゲシュタルトパターンマッチングは非可換(non-commutative)、すなわち交換できません。

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

以下の例で確認してみましょう。
2つの文字列を

  • $S_1$:Gestalt Pattern Matching
  • $S_2$:Gestalt Practice

とすると、マッチする文字列は以下のように異なります。

  • $D_{ro}(S_1, S_2)$:Gestalt P, a, t, e
  • $D_{ro}(S_2, S_1)$:Gestalt P, r, a, c, i

よって、それぞれ類似度を計算すると

\begin{align}
D_{ro}(S_1, S_2)&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"a"|+|"t"|+|"e"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(9+1+1+1)}{24+16}\\
&=\frac{24}{40}=0.6\\
\\
D_{ro}(S_2, S_1)&=\frac{2K_m}{|S_2|+|S_1|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"r"|+|"a"|+|"c"|+|"i"|})}{|S_2|+|S_1|}\\
&=\frac{2\cdot(9+1+1+1+1)}{16+24}\\
&=\frac{26}{40}=0.65\\
\end{align}
\begin{align}
\end{align}

となり、確かに

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

と非可換となっています。

実際に、コードで確認すると

str1 = 'GESTALT PATTERN MATCHING'
str2 = 'GESTALT PRACTICE'
print(difflib.SequenceMatcher(None, str1, str2).ratio())
print(difflib.SequenceMatcher(None, str2, str1).ratio())
# 0.6
# 0.65

と同様の結果を得ることができます。

参考文献

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

ゲシュタルトパターンマッチングによる文字列の類似度の計算

はじめに

2つの文字列の類似度を計算するアルゴリズムの1つである、
ゲシュタルトパターンマッチング
について調べたので、内容をまとめました。

※本記事は、Wikipedia(英語版)の内容をベースとしています。

目次

実装

ゲシュタルトパターンマッチングは、Pythonのdifflibで以下のように実装できます。
※環境は、Google Colaboratoryを使用

import difflib

word = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6))
# ['apple', 'ape']

ある文字列(word = 'appel')と似ているものを、文字列のリスト(possibilities = ['ape', 'apple', 'peach', 'puppy'])のなかから取り出すことができます。

get_close_matchesの使い方は以下です。

  • 引数
    • word:マッチさせたい文字列
    • possibilities:wordにマッチさせる文字列のリスト
    • n(デフォルト3):マッチさせる文字列の最大数。1以上を指定。
    • cutoff(デフォルト0.6):wordとの一致率。0以上1以下を指定。
  • 戻り値
    • マッチした文字列のリスト。cutoff以上の一致率の文字列リストを、一致率の高い順(似ている順)にソートして返却。

マッチさせる文字列の数はn、マッチングの厳しさはcutoffで調整できます。

# n=1
print(difflib.get_close_matches(word, possibilities, n=1, cutoff=0.6))
# ['apple']

# cutoff=0.8
print(difflib.get_close_matches(word, possibilities, n=3, cutoff=0.8))
# ['apple']

マッチの一致率は、difflibのSequenceMatcherで調べることができます。cutoffを決める際の参考になるかと思います。

str1 = 'appel'
possibilities = ['ape', 'apple', 'peach', 'puppy']
for str2 in possibilities:
  ratio = difflib.SequenceMatcher(None, str1, str2).ratio()
  print(str2, ratio)

# ape 0.75
# apple 0.8
# peach 0.4
# puppy 0.4

ゲシュタルトパターンマッチングとは

ゲシュタルトパターンマッチング(Gestalt Pattern Matching)』とは、2つの文字列の類似度を判定するためのアルゴリズムです。Ratcliff, Obershelpによって1983年に考案されました。Ratcliff/Obershelp Pattern Recognitionとも呼ばれているようです。
(以下、その頭文字をとって$D_{ro}$などと表記する)

「ゲシュタルト(Gestalt)」は、「形」「形態」「状態」といった意味を表すドイツ語です。

「ゲシュタルト崩壊」という言葉を聞いたことがある方も多いのではないでしょうか。ずっと同じ文字を眺めていると、それが何を表す文字なのかわからなくなってしまう感覚のことです。

ゲシュタルト崩壊(ゲシュタルトほうかい、独: Gestaltzerfall)とは、知覚における現象のひとつ。 全体性を持ったまとまりのある構造(Gestalt, 形態)から全体性が失われてしまい、個々の構成部分にバラバラに切り離して認識し直されてしまう現象をいう。幾何学図形、文字、顔など、視覚的なものがよく知られているが、聴覚や皮膚感覚、味覚、嗅覚においても生じうる。
Wikipediaより引用

「ゲシュタルト」のこれらの意味より、
「ゲシュタルトパターンマッチング」は、細かい部分は気にせず、あくまで文字列全体のまとまりに重きを置いて類似度を計算する手法
というのが個人的な印象です。

アルゴリズム

ゲシュタルトパターンマッチングによる、2つの文字列$S_1, S_2$の類似度$D_{ro}$は、以下で表されます。

D_{ro}(S_1, S_2)=\frac{2K_m}{|S_1|+|S_2|}

ここで

  • $K_m$:マッチした文字数
  • $|S_1|, |S_2|$:それぞれ$S_1, S_2$の文字列の長さ

であり、類似度$D_{ro}$の取り得る値の範囲は

0 \leq D_{ro} \leq 1

となります。
1に近いほど類似度は高く、0に近いほど類似度は低くなります。ちなみに、

  • $D_{ro}=1$:完全一致
  • $D_{ro}=0$:1文字もマッチしない

となります。

$S_1$ S I M I L A R I T Y
$S_2$ S I M I R E L I T Y

左から順に見ていくと、最長でSIMIの4文字がマッチしています。
残った文字列に対して再び見ていくと、最長でITYの3文字がマッチしています。

よって、この場合の類似度は以下になります。

\begin{align}
D_{ro}&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"SIMI"|+|"ITY"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(4+3)}{10+10}\\
&=\frac{7}{10}=0.7
\end{align}

性質

非可換性

ゲシュタルトパターンマッチングは非可換(non-commutative)、すなわち交換できません。

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

以下の例で確認してみましょう。
2つの文字列を

  • $S_1$:Gestalt Pattern Matching
  • $S_2$:Gestalt Practice

とすると、マッチする文字列は以下のように異なります。

  • $D_{ro}(S_1, S_2)$:Gestalt P, a, t, e
  • $D_{ro}(S_2, S_1)$:Gestalt P, r, a, c, i

よって、それぞれ類似度を計算すると

\begin{align}
D_{ro}(S_1, S_2)&=\frac{2K_m}{|S_1|+|S_2|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"a"|+|"t"|+|"e"|})}{|S_1|+|S_2|}\\
&=\frac{2\cdot(9+1+1+1)}{24+16}\\
&=\frac{24}{40}=0.6\\
\\
D_{ro}(S_2, S_1)&=\frac{2K_m}{|S_2|+|S_1|}\\
&=\frac{2\cdot(\mbox{|"Gestalt P"|+|"r"|+|"a"|+|"c"|+|"i"|})}{|S_2|+|S_1|}\\
&=\frac{2\cdot(9+1+1+1+1)}{16+24}\\
&=\frac{26}{40}=0.65\\
\end{align}
\begin{align}
\end{align}

となり、確かに

D_{ro}(S_1, S_2) \neq D_{ro}(S_2, S_1)

と非可換となっています。

実際に、コードで確認すると

str1 = 'GESTALT PATTERN MATCHING'
str2 = 'GESTALT PRACTICE'
print(difflib.SequenceMatcher(None, str1, str2).ratio())
print(difflib.SequenceMatcher(None, str2, str1).ratio())
# 0.6
# 0.65

と同様の結果を得ることができます。

参考文献

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

初心者 初めてのWEBアプリ(FASTAPI) 

初めてWEBアプリを開発しています。
FASTAPIをお薦めされ、このフレームワークを使用していますが、なかなか参考資料が少なく開発難航中です。(笑)

現在の困りごと:
トレーニングの計画や実施事項をまとめたスポーツのコンデションプランを視覚化できるアプリケーションを作っています。
ユーザーをスポーツチームでお互いのプランやトレーニング内容を確認できるようにアプリ内でユーザーをグループ化できるようにしたいのですが、どのようにコードを書くかに困っています。
グループ機能を組み込むには、どのようなロジックで書くか模索中!

記事として書いてますが、何か参考にグループ機能についてコメントに書いていただけますと嬉しいです!
絶賛、猛勉強中!!

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

manimの作法 その28

概要

manimの作法、調べてみた。
animation、全部使ってみた2。

サンプルコード

from manimlib.imports import *

class test(Scene):
    def construct(self):
        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text0").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[ShowCreation(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text1").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.wait(0.3)
        self.play(*[Uncreate(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        vmobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text2").scale(2))
        vmobjects.scale(1.5)
        vmobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[DrawBorderThenFill(mob) for mob in vmobjects])
        self.wait()
        self.play(FadeOut(vmobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text3").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[Write(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text4").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.wait(0.3)
        self.play(*[FadeOut(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text5").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[FadeIn(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text6").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        directions = [UP, LEFT, DOWN, RIGHT]
        for direction in directions:
            self.play(*[FadeInFrom(mob,direction) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text7").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[FadeInFromDown(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text8").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        directions = [UP, LEFT, DOWN, RIGHT]
        self.add(mobjects)
        self.wait(0.3)
        for direction in directions:
            self.play(*[FadeOutAndShift(mob,direction) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Text9").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[FadeOutAndShiftDown(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Texta").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        scale_factors = [0.3, 0.8, 1, 1.3, 1.8]
        for scale_factor in scale_factors:
            t_scale_factor = TextMobject(f"\\tt scale\\_factor = {scale_factor}")
            t_scale_factor.to_edge(UP)
            self.add(t_scale_factor)
            self.play(*[FadeInFromLarge(mob, scale_factor) for mob in mobjects])
            self.remove(t_scale_factor)
        self.wait(0.3)
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textb").scale(2))
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        directions = [UP, LEFT, DOWN, RIGHT]
        for direction in directions:
            self.play(*[GrowFromPoint(mob, mob.get_center() + direction * 3) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textc").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[GrowFromCenter(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textd").scale(2))
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        directions = [UP, LEFT, DOWN, RIGHT]
        for direction in directions:
            self.play(*[GrowFromEdge(mob, direction) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Arrow(LEFT, RIGHT), Vector(RIGHT * 2))
        mobjects.scale(3)
        mobjects.arrange_submobjects(DOWN, buff = 2)
        self.play(*[GrowArrow(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Square(), RegularPolygon(fill_opacity = 1), TextMobject("Texte").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[SpinInFromNothing(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Square(), RegularPolygon(fill_opacity = 1), TextMobject("Textf").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[ShrinkToCenter(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Dot(), TexMobject("x"))
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        mobject_or_coord = [*mobjects, mobjects.get_right() + RIGHT * 2]
        colors = [GRAY, RED, BLUE]
        self.add(mobjects)
        for obj,color in zip(mobject_or_coord, colors):
            self.play(FocusOn(obj, color = color))
        self.wait()
        self.play(FadeOut(mobjects))

        formula = TexMobject("f(", "x", ")")
        dot = Dot()
        VGroup(formula, dot).scale(3).arrange_submobjects(DOWN, buff = 3)
        self.add(formula, dot)
        for mob in [formula[1], dot]:
            self.play(Indicate(mob))
        self.wait()
        self.remove(formula)
        self.remove(dot)

        mobjects = VGroup(Dot(), TexMobject("x0")).scale(2)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        mobject_or_coord = [*mobjects, mobjects.get_right() + RIGHT * 2]
        colors = [GRAY, RED, BLUE]
        self.add(mobjects)
        for obj,color in zip(mobject_or_coord,colors):
            self.play(Flash(obj, color = color, flash_radius = 0.5))
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Dot(), TexMobject("x1")).scale(2)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.wait(0.2)
        for obj in mobjects:
            self.play(CircleIndicate(obj))
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textg").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[ShowCreationThenDestruction(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Texth").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.play(*[ShowCreationThenFadeOut(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Texti").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[ShowPassingFlashAround(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textj").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[ShowCreationThenDestructionAround(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textk").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[ShowCreationThenFadeAround(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textl").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[ApplyWave(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textm").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[WiggleOutThenIn(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobjects = VGroup(Circle(), Circle(fill_opacity = 1), TextMobject("Textn").scale(2))
        mobjects.scale(1.5)
        mobjects.arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[TurnInsideOut(mob) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        mobject = RegularPolygon(3).scale(2)
        self.add(mobject)
        for n in range(4, 9):
            self.play(Transform(mobject, RegularPolygon(n).scale(2)))
        self.wait(0.3)
        self.play(FadeOut(mobject))

        polygons = [*[RegularPolygon(n).scale(2) for n in range(3, 9)]]
        self.add(polygons[0])
        for i in range(len(polygons) - 1):
            self.play(ReplacementTransform(polygons[i], polygons[i + 1]))
        self.wait(0.3)
        self.remove(polygons[0])
        self.remove(polygons[1])
        self.remove(polygons[2])
        self.remove(polygons[3])
        self.remove(polygons[4])
        self.remove(polygons[5])



        polygons = VGroup(*[RegularPolygon(n).scale(0.7) for n in range(3, 9)]).arrange_submobjects(RIGHT, buff = 1)
        self.add(polygons[0])
        for i in range(len(polygons) - 1):
            self.play(ClockwiseTransform(polygons[0], polygons[i + 1]))
        self.wait(0.3)
        self.play(FadeOut(polygons))

        polygons = VGroup(*[RegularPolygon(n).scale(0.7) for n in range(3, 9)]).arrange_submobjects(RIGHT, buff = 1)
        self.add(polygons[0])
        for i in range(len(polygons) - 1):
            self.play(CounterclockwiseTransform(polygons[0], polygons[i + 1]))
        self.wait(0.3)
        self.play(FadeOut(polygons))

        mobject = Square()
        mobject.generate_target()
        VGroup(mobject, mobject.target).arrange_submobjects(RIGHT, buff = 3)
        mobject.target.rotate(PI / 4).scale(2).set_stroke(PURPLE, 9).set_fill(ORANGE, 1)
        self.add(mobject)
        self.wait(0.3)
        self.play(MoveToTarget(mobject))
        self.wait(0.3)
        self.play(FadeOut(mobject))

        dot = Dot()
        text = TextMobject("Texto")
        dot.next_to(text, LEFT)
        self.add(text, dot)
        self.play(ApplyMethod(text.scale, 3, {
            "about_point": dot.get_center()
        }))
        self.wait(0.3)
        self.remove(dot)
        self.remove(text)

        text = TextMobject("Textp")
        self.add(text)
        def spread_out(p):
            p = p + 2 * DOWN
            return (FRAME_X_RADIUS + FRAME_Y_RADIUS) * p / get_norm(p)
        self.play(ApplyPointwiseFunction(spread_out, text))
        text = TextMobject("Text").set_width(FRAME_WIDTH)
        colors = [RED, PURPLE, GOLD, TEAL]
        self.add(text)
        for color in colors:
            self.play(FadeToColor(text, color))
        self.wait(0.3)
        self.remove(text)

        text = TextMobject("Textq").set_width(FRAME_WIDTH / 2)
        scale_factors = [2, 0.3, 0.6, 2]
        self.add(text)
        for scale_factor in scale_factors:
            self.play(ScaleInPlace(text, scale_factor))
        self.wait(0.3)
        self.remove(text)

        text = TextMobject("Original").set_width(FRAME_WIDTH / 2)
        text.save_state()
        text_2 = TextMobject("Modified").set_width(FRAME_WIDTH / 1.5).set_color(ORANGE).to_corner(DL)
        self.add(text)
        self.play(Transform(text, text_2))
        self.play(text.shift, RIGHT, text.rotate, PI / 4)
        self.play(Restore(text))
        self.wait(0.7)
        self.remove(text)
        self.remove(text_2)

        text = TextMobject("Texts").to_corner(DL)
        self.add(text)
        def apply_function(mob):
            mob.scale(2)
            mob.to_corner(UR)
            mob.rotate(PI / 4)
            mob.set_color(RED)
            return mob
        self.play(ApplyFunction(apply_function, text))
        self.wait(0.3)
        self.remove(text)

        def plane_wave_homotopy(x, y, z, t):
            norm = get_norm([x, y])
            tau = interpolate(5, -5, t) + norm / FRAME_X_RADIUS
            alpha = sigmoid(tau)
            return [x, y + 0.5 * np.sin(2 * np.pi * alpha) - t * SMALL_BUFF / 2, z]
        mobjects = VGroup(TextMobject("Text").scale(3), Square(), ).arrange_submobjects(RIGHT, buff = 2)
        self.add(mobjects)
        self.play(*[Homotopy(plane_wave_homotopy, mob) for mob in mobjects])
        self.wait(0.3)
        self.play(FadeOut(mobjects))

        def func(t):
            return t * 0.5 * RIGHT
        mobjects = VGroup(TextMobject("Textt").scale(3), Square(), ).arrange_submobjects(RIGHT, buff = 2)
        self.play(*[PhaseFlow(func, mob, run_time = 2,) for mob in mobjects])
        self.wait()
        self.play(FadeOut(mobjects))

        line = Line(ORIGIN, RIGHT * FRAME_WIDTH, buff = 1)
        line.move_to(ORIGIN)
        dot = Dot()
        dot.move_to(line.get_start())
        self.add(line, dot)
        self.play(MoveAlongPath(dot, line))
        self.wait(0.3)
        self.remove(line)
        self.remove(dot)

        square = Square().scale(2)
        self.add(square)
        self.play(Rotating(square,radians = PI / 4, run_time = 2))
        self.wait(0.3)
        self.play(Rotating(square,radians = PI, run_time = 2, axis = RIGHT))
        self.wait(0.3)
        self.remove(square)

        square = Square().scale(2)
        self.add(square)
        self.play(Rotate(square, PI / 4, run_time = 2))
        self.wait(0.3)
        self.play(Rotate(square, PI, run_time = 2, axis = RIGHT))
        self.wait(0.3)
        self.remove(square)
        self.wait()

        mobject = RegularPolygon(3).scale(2)
        self.add(mobject)
        for n in range(4, 9):
            self.play(TransformFromCopy(mobject, RegularPolygon(n).scale(2)))
        self.wait(0.3)
        self.play(FadeOut(mobject))
        self.wait()





生成した動画

https://www.youtube.com/watch?v=a3blYTllCF0

以上。

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

Python の __name__ を理解する

Python で 以下のようなコードをよく目にする。

if __name__ == "__main__":

これについては色々な記事があるが、自分なりにコードを実行してみてみるのが理解し易いため、ハンズオン形式で理解できるようにメモ。

結論

__name__ は、

  • 実行ファイル内では __main__

  • 外部から読み込まれた場合は 拡張子なしのファイル名

となる。

上記のことをつらつらと確認していく。

ディレクトリ構成

下記の構成を用意して動作確認した。

.
├── test1.py
└── test2.py

__name__ を出力してみる

test1.py 内で __name__ を出力するようにして実行する。

test1.py
print('test1: ' + __name__)
> .% python test1.py
> test1: __main__

__name__ は __main__ と出力された。

外部ファイルから読み込んでみる

test2.py から test1.py を読み込んだ上で、test2.py 内でも __name__ を出力するようにして、test2.py を実行する。

test2.py
import test1

print('test2: ' + __name__)
> .% python test2.py
> test1: test1
> test2: __main__

test1.py の __name__ は test1 と出力され、test2.py の __name__ は __main__ と出力された。

つまり、実行ファイル内では __name__ が __main__ となり、外部から読み込まれた場合は __name__ が 拡張子なしのファイル名 となる。

結論

__name__ は、

  • 実行ファイル内では __main__

  • 外部から読み込まれた場合は 拡張子なしのファイル名

となる。

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

【Python】エラーメッセージの内容を理解する(2)

背景

自身がPython習得をすすめる上での備忘録ではありますが、エラーメッセージの読み解き方を、ここにまとめておきます。
前回、掲載しきれなかったエラーを掲載した『第二弾』です。

先人たちの知恵をお借りするなどして解決できたことを、この場をお借りして感謝するとともに、大変恐縮ですが自分のメモとしても、こちらへまとめておきます。

開発環境

  • Windows 10 Pro
  • Python 3.9.0, 3.8.5
  • Django 3.1.3
  • PostgreSQL 13.1
  • Nginx 1.19.5
  • Gunicorn
  • Putty 0.74

エラーメッセージ

9. ImportError(インポート・エラー)

  • from <モジュール名> import <オブジェクト名、関数名 など)> でモジュールに含まれていないオブジェクトをインポートしようとした場合などに発生する。大文字・小文字も区別されるため、タイプミスに注意。
python
from bs4 import BeautifulSoap    # 誤...Soap→Soup

from bs4 import BeautifulSoup    # 正
ターミナル
ImportError: cannot import name 'BeautifulSoap' from 'bs4' (/usr/local/~/python/3.9.0/・・・)

【訳】
インポート(取り込み)エラーが発生:「bs4」から「BeautifulSoap」という名前のモジュールを取り込めません。
 ↓
『「BeautifulSoap」って名前のモジュールが取り込めないよ~!』ということ。

※綺麗な石鹸(Soap)じゃなくて、美味しいスープ(Soup)ですよ~!^^

<注>
モジュール名やオブジェクト名は正しいか、対象のモジュールにそのオブジェクトが含まれているかは、公式ドキュメントなどを確認する癖を付けましょう
稀にライブラリがバージョンアップされた際に、そのオブジェクト名が変わったり、無くなったり(廃止されたり)します

10. IndexError(インデックス・エラー)

  • リストやタプルなどシーケンスオブジェクトに格納された値を [インデックス] で取得する際、その範囲外の位置(要素数を超えたインデックス値)を指定することで起こる。
python
list1 = ["あ", "い", "う", "え", "お"]

print(list1[5])        # 誤...末尾の "お" は、インデックスでは [
要素数-1] であるため4 と指定するのが正解

print(list1[4])        # 正
# お
ターミナル
IndexError: list index out of range

【訳】
索引エラーが発生:リストの索引は範囲外です。
 ↓
『リスト型の索引の範囲外だよ~(...だから探しに行けないの...)ということ。

 
リストやタプルの要素数は len() 関数で確認可能です。よく確認してからインデックスを指定しましょう。
ちなみに、インデックスは 0 から数えはじめますので、末尾は 要素数-1 番目になります。

python
list1 = ["あ", "い", "う", "え", "お"]

print(len(list1))
# 5

11. KeyError(キー・エラー)

  • 辞書型(dict型)の値をキー指定して取得する際、存在しないキーを指定してしまうと起こる。
python
dict1 = {1 : "りんご" , 2 : "みかん", 3 : "ぶどう" , 4 : "バナナ"}

print(dict1[5])    # 誤...'5' は、Key として存在しない

print(dict1[4])    # 正
# バナナ
ターミナル
KeyError: 5

【訳】
キーエラーが発生:'5'(が、キーエラーです。)
 ↓
(そのまま)『5 は キーエラーですな~』ということ。

 
キーの一覧は、keys() メソッドで確認しましょう。

python
dict1 = {1 : "りんご" , 2 : "みかん", 3 : "ぶどう" , 4 : "バナナ"}

print(dict1)
# {1: 'りんご', 2: 'みかん', 3: 'ぶどう', 4: 'バナナ'}

print(list(dict1.keys()))
# [1, 2, 3, 4]

 
裏技?
get() メソッドを使うと、存在しないキーに対してもエラーになりません。
この場合、デフォルト値を取得することができます!

python
print(dict1.get[5])

# None

辞書に含まれるキーや、指定したキーが正しいかを、よく確認しましょう。
(指差し確認、安全よーし!!)

12. AttributeError(アトリビュート・エラー)

  • 属性(Attribute)の参照に関して起こる。

「<オブジェクト>.<識別子>」のようにメソッドなどを呼び出す際、オブジェクトや識別子(属性やメソッド)の名前、オブジェクトの型を間違えた場合に発生する。
大文字・小文字も区別されるため、タイプミスに注意。

12-1.

  • 識別子の名前を間違えた例:
python
import math

print(math.PI)        # 誤...PI→pi

print(math.pi)        # 正
ターミナル
AttributeError: module 'math' has no attribute 'PI'

【訳】
属性エラー発生:'math' モジュールは 'PI' という属性を持ち合わせません。
 ↓
『「math」は「PI」なんて、持ってないってさ!』ということ。

12-2.

  • オブジェクトの型が想定と違った例:
python
counter = 60

counter.append(120)
ターミナル
AttributeError: 'int' object has no attribute 'append'

【訳】
属性エラー発生:'int' オブジェクト(数値型オブジェクト)は 'append' という属性を持ち合わせません。
 ↓
『数値型オブジェクトに「append」なんて、備わってないってよ~』ということ。

オブジェクト名、識別子名や、オブジェクトの型は想定通りか、をよく確認しましょう。

13. FileNotFoundError(ファイル・ノット・ファンド・エラー)

  • ファイルを読み込む際など open() で指定したファイルが見つからないときに起こる。
python
with open('sample1.txt') as f:    # sample1.txt は存在しない
    print(f.read())
ターミナル
FileNotFoundError: [Errno 2] No such file or directory: 'sample1.txt'

【訳】
ファイルが見付からないエラーが発生:'sample1.txt' というファイルまたはディレクトリはありません。
 ↓
『「sample1.txt」 は ありまへんがな~』ということ。

 
指定したファイルやディレクトリは本当に存在している?
指定したパスは正しい?特に、相対パスで指定したときのカレントディレクトリは正しい?
(相対パスで指定した際、Pythonのカレントディレクトリが想定と異なっている可能性も考えられます。)

14. FileExistsError(ファイル・イグジスツ・エラー)

  • ファイルやディレクトリを作成しようとした際に、それが既に存在したときに起こる。(13.の逆パターン)
python
import os
os.mkdir('sub_dir')        # 'sub_dir' は既に存在している
ターミナル
FileExistsError: [Errno 17] File exists: 'sub_dir'

【訳】
ファイルは存在するエラーが発生:'sub_dir' というファイル(またはディレクトリ)は存在します。
 ↓
『「sub_dir」 は もう在るやん!』ということ。

指定したファイル(またはディレクトリ)はすでに存在していない?

<注>
python 3.2.x 以降では、os.makedirs() に引数 exist_ok が追加されています。
このため、exist_ok=True とした際には、既に存在しているディレクトリの作成を試みてもエラーとなりません。


(編集後記)

よく遭遇するエラーを集めてまとめてみました、の第二弾です。
エラーの内容、原因・理由を理解して、複雑なプログラミングをする際に、エラーが出ても慌てないようすることと、対処を適切に素早く施せるようにしたいですね。

VS Code には、リアルタイムに文法をチェックしてくれる拡張機能もあるようですので、いろいろ試してみようと思います。

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

Flask APIでNo 'Access-Control-Allow-Origin...エラーの対処法

ReactからFlaskAPIを呼び出そうとしたらこんなエラーが。

No 'Access-Control-Allow-Origin.Origin 'null' is therefore not allowed access.' header is present on the requested resource error

JavaScriptが別のプロセスでホストされているHTTPリクエストを送ろうとすると出るエラーなので、Flask側で CORS (Cross-Origin Resource Sharing)を有効にする。

1.flask-corsをインストール

pip install -U flask-cors

2.設定

main.py
# -*- coding: utf-8 -*-
from flask import Flask, jsonify
from flask_cors import CORS # <-追加
import json

app = Flask(__name__)
CORS(app) # <-追加

@app.route('/', methods=['GET'])
def sample():
    ...

これだけ!

Enjoy Hacking!?

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

サーマルカメラ(サーモ AI デバイス TiD) Python ヒートマップ作成

はじめに

サーマルカメラとしては、取得した温度データをヒートマップにしたいのです。
MH ソフトウェア&サービスでは、Raspberry Piを使用して実現したいので、温度データ -> ヒートマップ変換は、4種類ほど作成してみました。(高速化が必要だったのです。)

使用機材

サーマルカメラ(サーモ AI デバイス TiD) Python AMG8833 番外編で使用したwebカメラとPanasonic AMG8833、Raspberry Piを使用して取得した温度データを使用してみます。
カメラ&AMG8833.JPG
テスト用にwebカメラのレンズ付近に、Panasonic AMG8833を取り付けているだけの簡素な仕様です。
温度データは
for_gif_320x240_fps2.gif
この動画の一部の、指を開いている個所の温度データです。回転処理をしていませんので、ヒートマップの画像は180度回転しています。

メインクラス(HeatMap)

このクラスからversion1.py ~ version4.pyを開きます。
それぞれ10回の処理を実行して、処理速度を比較します。処理はRaspberry Pi 4を使用しています。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np
import PIL
import PIL.Image
import PIL.ImageTk
import time

import cv2

from version1 import Version1
from version2 import Version2
from version3 import Version3
from version4 import Version4


class HeatMap():
    def __init__(self):
        self.heat_map = [
            None,
            Version1(),
            Version2(),
            Version3(),
            Version4()
        ]

    def __call__(self):
        img = None
        timer = ProcessingTime()

        # Version1から4まで処理速度を比較してみます
        for version in range(1, 5):
            # 計測タイマリセット
            timer.get()
            # 処理回数は10回です
            for _ in range(10):
                img = self.heat_map[version](self.parameters)

            message = f'Version{version} {timer.get():.02f}[msec]'
            print(message)

            img = resize(img, self.parameters.size, self.parameters.size)
            cv2.imshow(message, img)
            cv2.waitKey(5000)
        while True:
            pass

    class parameters():
        gain = 10
        max = 40
        min = 20
        offset_x = 0.2
        offset_green = 0.6
        pixels = 8 * 8
        size = 320

class ProcessingTime():
    """
    Return processing time [msec].
    """
    def __init__(self):
        self._start = time.time()

    def get(self) -> float:
        result = (time.time() - self._start) * 1000
        self._start = time.time()
        return result


def resize(image: np.ndarray, width: int, height: int) -> np.ndarray:
    result = cv2.resize(image, (width, height), cv2.INTER_CUBIC)
    return result


if __name__ == '__main__':
    heat_map = HeatMap()
    heat_map()

Version1クラス

webからサンプルを取得して作成したクラスです。ごめんなさい。引用元が不明になってしまいました。
version1.jpg

処理時間: 3,907[msec]

ヒートマップは、指だな?と感じられます。
for文を多用しているせいか、これ以上の高速化は望めませんでした。色のブレンドはカッコ良いと思います。元ソースに感謝です。

version1.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import math

from colour import Color
import numpy as np
from scipy.interpolate import griddata

from temp_data import temp_data


class Version1():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        color_depth = 1024
        pixels = temp_data()
        pixels = [
            map_value(
                p, parameters.min, parameters.max, 0, color_depth - 1)
                for p in pixels]

        sqrt = int(parameters.pixels ** 0.5)
        points = [(
            math.floor(ix / sqrt),
            (ix % sqrt)) for ix in range(0, sqrt * sqrt)]

        grid_x, grid_y = np.mgrid[
            0: (sqrt - 1): 32j,
            0: (sqrt - 1): 32j]

        bicubic = griddata(
            points,
            pixels,
            (grid_x, grid_y),
            method='cubic')

        low = Color('darkblue')
        high = Color('red')
        colors = list(low.range_to(high, color_depth))

        img = [[[255] * 3] * bicubic.shape[0]] * bicubic.shape[1]
        img = np.asarray(img)

        # やはりfor文を使うと時間がかかります
        for ix, row in enumerate(bicubic):
            for jx, pixel in enumerate(row):
                rgb = colors[
                    constrain(
                        int(pixel),
                        0,
                        color_depth- 1
                    )].rgb
                img[ix, jx, 0] = rgb[2] * 255
                img[ix, jx, 1] = rgb[1] * 255
                img[ix, jx, 2] = rgb[0] * 255
        img = img.astype('uint8')
        return img


def constrain(val: int, min_val: int, max_val: int) -> int:
    return min(max_val, max(min_val, val))


def map_value(x: int, in_min: int, in_max: int,
              out_min: int, out_max: int) -> float:
    result = 0
    try:
        result = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
    except:
        pass
    return result


if __name__ == '__main__':
    pass

Version2クラス

Version1クラスで、高速化が望めず、それならばnumpyで処理!と意気込んで作成したクラスです。
version2.jpg

処理時間: 25[msec]

ヒートマップは、なんとか指だな?と感じられます。
黄色が足らない・・・。と感じます。ほぼ、青と赤ですね・・・。
数値と色変換が甘く、赤成分と青成分のみ演算しています。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np

from temp_data import temp_data


class Version2():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        sqrt = int(parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')
        pitch = (parameters.max - parameters.min) / 255

        for x in range(sqrt):
            for y in range(sqrt):
                temp = temp_data()[x * 8 + y]
                img[x, y, 2] = (temp - parameters.min) / pitch
                img[x, y, 0] = (parameters.max - temp) / pitch

        return img


if __name__ == '__main__':
    pass

Version3クラス

version3.jpg

処理時間: 3[msec]

もはや、何が表示されているかわかりません。
緑成分を入れようと作成を始めたのですが、温度から緑ってどうするんだろう?と悩んだ結果です。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np

from temp_data import temp_data


class Version3():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        temp = np.array(temp_data())
        temp *= 1.5
        temp = temp.reshape(8, 8)

        blue = temp - parameters.min
        green = 0
        red = parameters.max - temp

        sqrt = int(parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')
        pitch = (parameters.max - parameters.min) / 255
        img[:, :, 0] = 255 - blue / pitch
        img[:, :, 1] = green
        img[:, :, 2] = 255 - red / pitch

        return img


if __name__ == '__main__':
    pass

Version4クラス

version4.jpg

処理時間: 10[msec]

ヒートマップは、指だな?と感じられます。
webでいろいろ情報を集めたところ、シグモイド関数を使うと、良いらしいことが分かりました。
画像データをnumpy.ndarrayで処理できるように、def sigmoid()を作成しました。

現在のサーモ AI デバイス TiDは、この処理を用いています。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np
from temp_data import temp_data


class Version4():
    def __call__(self, parameters: dict) -> np.array:
        self.parameters = parameters

        width = self.parameters.max - self.parameters.min
        temp = np.array(temp_data())
        temp = temp - self.parameters.min
        temp = temp / width
        temp = temp.reshape(8, 8)

        sqrt = int(self.parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')

        img[:, :, 0]\
            = self.color_bar_rgb(temp)[0]
        img[:, :, 1]\
            = self.color_bar_rgb(temp)[1]
        img[:, :, 2]\
            = self.color_bar_rgb(temp)[2]

        return img


    def color_bar_rgb(self, x: np.ndarray) -> list:
        """
        Return list of numpy.ndarray.
        """
        x = (x * 2) - 1
        red = sigmoid(x, self.parameters.gain, -1 * self.parameters.offset_x)
        blue = 1 - sigmoid(x, self.parameters.gain, self.parameters.offset_x)

        green = sigmoid(x, self.parameters.gain, self.parameters.offset_green)
        green += (1 - sigmoid(
            x, self.parameters.gain, -1 * self.parameters.offset_green))
        green = green - 1.0

        blue = blue * 255
        green = green * 255
        red = red * 255

        return [blue, green, red]


def sigmoid(x: np.ndarray, gain=1, offset_x=0) -> np.ndarray:
    return ((np.tanh(((x + offset_x) * gain) / 2) + 1) / 2)


if __name__ == '__main__':
    pass

まとめ

Version1 Version2 Version3 Version4
version1.jpg version2.jpg version3.jpg version4.jpg
指です 指です なんとなく指かも? 指です

サーマルカメラ(サーモ AI デバイス TiD) Python AMG8833 番外編

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