とりあえずなんか書いとけ

Python3系とかアイワナ(GM8.1 Standard)とか読書とか

Pythonの高速化について

この記事では,Pythonの実行高速化手法について書いていきます.
あくまでも個人の意見であり,どの程度正しいかわからないので適宜調べてください.
今回は主に

  • プロファイリングによる高速化(cProfile)
  • NumPyにおける高速化
  • コンパイルによる高速化(Cython, Numba)

について書いていこうと思います.
個人的な意見ですが,優先度は
プロファイリング >> (使ってるならNumPy) >> コンパイル
だと思ってます.
遅いなと思ったらとりあえずプロファイリングしましょう.

プロファイリングによる高速化(cProfile)

書いてある通り,cProfileモジュールを使ってプロファイリングをします.
プロファイリングというのは,ソースコードを実行したときにどの関数が何回実行されているか,実行ごとにどのくらいの時間がかかっているかなどの情報を得ることで,これをすることで時間がかかっている処理を特定することができます.
前に書いた記事でも名前だけ出している方法です.
toki0177.hatenablog.com
パレートの法則の例としてよく言われる「プログラムの処理にかかる時間の80%はコード全体の20%の部分が占める。」というものがあります.
これはつまりコード全体の20%分について実行時間を減らすことができれば大幅な高速化が見込めるということです.
それではやっていきましょう!


プロファイリングを行うコードは以下のようになっています.

import cProfile
import pstats
 
pr = cProfile.Profile()
pr.enable()
# ここにプロファイリングしたい処理を書く
pr.disable()
stats = pstats.Stats(pr)
stats.sort_stats('cumtime')
stats.print_stats()
pr.dump_stats('profile.stats')

ここでの処理の流れは

  1. Profileクラスのインスタンスを作成
  2. enableでプロファイリングを開始
  3. disableでプロファイリングを終了
  4. cumtime(総実行時間)で並び替え
  5. プロファイリング結果の出力
  6. ファイルに書き出し

のようになっています.
enableとdisableの間に記述した処理について実行時間などの計測を行います.
プロファイル結果において注目すべきは総実行時間や呼び出し回数,呼び出しごとの実行時間などいろいろなものがありますが,その時々で適切な部分を見ましょう.
これを実行すると,以下のような出力がされると思います.

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    130/1    0.004    0.000   22.159   22.159 {built-in method builtins.exec}
        1    0.000    0.000   22.159   22.159 hoge.py:1(<module>)
        1    0.001    0.001   21.937   21.937 hoge.py:12(main)
        1    0.017    0.017    9.810    9.810 hoge.py:154(hoge1)
       11    4.256    0.387    9.397    0.854 hoge.py:175(hoge2)
    65667    1.524    0.000    4.314    0.000 hoge.py:149(hoge3)
   131334    0.092    0.000    2.066    0.000 {method 'sum' of 'numpy.ndarray' objects}
   161078    2.059    0.000    2.059    0.000 {method 'reduce' of 'numpy.ufunc' objects}

これはある程度実行時間を短縮した後のものなのでここから変更するべき部分はあまりないかもしれませんが,実行時の様子を調べることができます.
今回は実行時間について調べましたが,その他にも

  • print_callers() : 呼び出した関数の情報
  • print_callees() : 呼び出された関数の情報

なども調べることができます.

このプロファイリングの結果を見て,実行が多すぎる部分を調べたり,1回ごとの実行時間が長い処理を調べてそこの実行時間を短縮すればいいということになります.
次に,プロファイリングの結果で「NumPyを使ってる部分の処理に時間がかかっている!」というときの対処について考えていきます.

NumPyにおける高速化

ここではNumPyにおける高速化の例を示していきます.
最も肝心なのはfor文をできる限り使わないことです.
たぶんどのページ見ても同じようなことを言われているとは思いますが,Pythonのfor文は遅いです.
NumPyはfor文を用いなくてもほとんどの処理ができると思っているので,できる限りfor文を使わない表記にしましょう.
例えば下のような関数を書いたとします.
適当に作りましたが2つの画像の各画素のユークリッド距離の総和を求める関数です.

def calc_distance(img1, img2):
    tmp = cv2.absdiff(img1, img2).astype(np.uint64)
    ret = 0
    for i in range(len(tmp)):
        for j in range(len(tmp[i])):
            ret += np.sqrt(tmp[i,j,0]**2+tmp[i,j,1]**2+tmp[i,j,2]**2)
    return ret

このコードをtimeitで計測した結果は以下の通りです.

f:id:toki_0177:20190310124000p:plain
高速化前の関数の計測結果

次に,for文を無くして高速化したコードは以下の通りです.

def calc_distance(img1, img2):
    tmp = cv2.absdiff(img1, img2).astype(np.uint64)
    return np.sqrt((tmp ** 2).sum(2)).sum()

行っている処理の内容はほぼ同じですが,timeitで計測した結果は以下の通りです.

f:id:toki_0177:20190310124309p:plain
高速化後の関数の計測結果
遅い方のコードをわざとかなり遅くしたのもありますが,桁だけで考えると100倍程度の高速化ができていることがわかります.
このようにNumPyではfor文を使わなくても処理ができることが多いので,できる限りfor文を使わないようにしましょう.

コンパイルによる高速化(Cython, Numba)

これについてはあまり詳しくは書きません.
調べるとたくさん出てくるので調べましょう.
これで高速化できる場合とあまり効果がない場合がある気がします.

簡単に言うと,Pythonは1行ずつ翻訳して実行していく(たぶん)のですが,コンパイルして実行するC言語などと比較すると低速です.
そこで,コンパイルをして実行を高速化しようとするという考えで作られたのがCythonとNumbaです(たぶん).
Cythonは実行前にコンパイルし,Numbaは実行時にコンパイルみたいな感じだと思います.


Pythonの実行が遅い理由の一つとして言われるのが型の宣言をしないので参照のときに型を調べる必要がありますが,コンパイルによる高速化以外にも明示的な型宣言をすることで高速化をすることもできます.

まとめ

とりあえずプロファイリングをしましょう.
今回言いたかったのはだいたいそれです.

Pythonで音声認識をやってみる

なんかやってみたかったので,PythonのSpeechRecognitionというのを使って音声認識をやってみます.
今回は基本的に日本語の音声認識を目的としてやっていきます.
また,今回は強いと噂のGoogleのSpeech APIを使用します.

SpeechRecognition,PyAudioのインストール

音声認識をするために必要なSpeechRecognitionと,音声入力に必要なPyAudioのインストールを行います.
コンソールとかで以下のコマンドを実行.

$ python -m pip install SpeechRecognition pyaudio

これでライブラリが使えるようになります.

Google Speech APIの有効化

今回は認識にGoogle Speech APIを使うので,これを使えるように登録をしていきます.
やり方は以下の記事を参考にしました.
qiita.com

ここでは割愛.

ファイルから音声認識

ここからはSpeechRecognitionを用いて音声認識をしていきたいと思います.
まずは入力としてファイルを使う方法から.
ソースコードは以下のものを使いました.
音声ファイルは作業ディレクトリにおいて,上の方法で取得したAPIのキーを用いて行います.

import speech_recognition
 
r = speech_recognition.Recognizer()
with speech_recognition.AudioFile('hoge.wav') as src:
    audio = r.record(src)
print(r.recognize_google(audio, key='Your API Key', language='ja-JP'))

今回は,音声データとして以下のURLの「模擬公演音声」をWAVファイルに変換したものを利用しました.
サンプル・データ 日本語話し言葉コーパス(CSJ)

実行結果は以下の通りになりました.

f:id:toki_0177:20190226144746p:plain
ファイルで入力したときの実行結果
さきほどのURLには音声ファイルをテキストに起こしたものもあるので,それと比較してみます.テキストに起こしたものを同じように表示したのがこちら.
f:id:toki_0177:20190226185948p:plain
入力したファイルを文字に起こしたもの

正確な結果の評価方法はわからないので適当ですが,この画像を見ればわかる通りかなり正確に音声認識できていることがわかります.

マイクから音声認識

次に入力としてマイクを使う方法について.
こちらも基本は同じで録音した音声をWAVファイルとして保存し,それを読み込ませることでマイクから音声認識を行います.
マイクからWAVファイルにするプログラムは,以下のサイトのソースコードを参考にしました.
Python3で録音してwavファイルに書き出すプログラム | 全人類がわかる統計学

実装したソースコードは以下の通りです.
ここでは録音時間は10秒としましたが,キー入力があるまでとかにできればもっと使いやすいかも.
あとはmain()内でループさせれば連続で入力することも可能?

import speech_recognition
import pyaudio
import wave
 
RECORD_SECONDS = 10         # 録音する時間
FILENAME = 'record.wav'     # 保存するファイル名
iDeviceIndex = 0            # 録音デバイスの番号
FORMAT = pyaudio.paInt16    # 音声フォーマット
CHANNELS = 1                # チャンネル数(モノラル)
RATE = 44100                # サンプリングのレート
CHUNK = 2**11               # データ点数
 

def main():
    record()
    recognition()

 
def record():
    audio = pyaudio.PyAudio()
    stream = audio.open(format=FORMAT, channels=CHANNELS,
                        rate=RATE, input=True,
                        input_device_index=iDeviceIndex,
                        frames_per_buffer=CHUNK)

    print("recording...")       # 録音開始
    frames = []
    for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
        data = stream.read(CHUNK)
        frames.append(data)
    print("finish recording")   # 録音終了
 
    stream.stop_stream()
    stream.close()
    audio.terminate()
 
    waveFile = wave.open(FILENAME, 'wb')
    waveFile.setnchannels(CHANNELS)
    waveFile.setsampwidth(audio.get_sample_size(FORMAT))
    waveFile.setframerate(RATE)
    waveFile.writeframes(b''.join(frames))
    waveFile.close()
 
def recognition():
    r = speech_recognition.Recognizer()
    with speech_recognition.AudioFile(FILENAME) as src:
        audio = r.record(src)
    print(r.recognize_google(audio, key='Your API Key', language='ja-JP'))
 
if __name__ == '__main__':
    main()


実際に動かしてみましょう.
読み上げる内容は,Wikipediaの赤血球についての記事の冒頭です.
赤血球 - Wikipedia
実行結果は以下の通りです.

f:id:toki_0177:20190226195017p:plain
マイク入力での実行結果
完璧に認識されています.
何回かやってみましたが,毎回かなりの精度で認識できていました.

感想とか

使ってみるまでは正直ここまでの精度で認識できると思っていなかったので驚いています.
さすがGoogleといったところでしょうか.
特に使いたい場所があるわけでもないんですがね.
あんまり調べてないからわかりませんが,Google Speech APIのリクエスト回数に制限がないのなら,自分の声をリアルタイムに文字に起こして,それを何らかの音声読み上げシステムに渡してあげることでリアルタイムに肉声を変換できるのでは?とか思いました(長文).
生配信とかで自分の声出したくない人には使えそう.

アイワナ:sinカーブを傾ける

タイトルではsinカーブとなっていますが,この記事では任意のオブジェクトの移動を傾ける方法について書いていきます.
簡単に言うと,下の図の左のようなsinカーブを右のような傾いたsinカーブにできるみたいな感じです.
sinカーブ以外にも使えますが,今回はsinカーブを傾けるものとして作成していきます.
※めんどくさいので弧度とか度数とかについては適当でいきます.

f:id:toki_0177:20190206130936p:plain
作れるもののイメージ

媒介変数表示について

これを作成する前に,まずsinカーブで移動させる方法を書いておきましょう.
sinカーブのような任意の曲線に従って移動させるには媒介変数表示というものを使います.
あまり簡単に説明できないので必要ならググってもらうとして,x,y座標を媒介変数という別の変数を用いて表す方法です.
たとえば円の媒介変数表示は
\left \{
\begin{array}{}
x=\cos \theta\\
y=\sin \theta
\end{array}
\right.
になりますが,これをGameMakerで実現しようとすると以下のようなコードを,動かしたいオブジェクトのstepイベントに書けばいいことになります.

//step event
x = center_x + 32 * cos(degtorad(theta))
y = center_y + 32 * sin(degtorad(theta))
theta += 1

こうすると(center_x, center_y)を中心とした半径32ドットの円を360ステップで1周するオブジェクトが完成します.


同様にして,sinカーブを考えてみましょう.
sinカーブを媒介変数表示で表すと下のようになります.
\left \{
\begin{array}{}
x=\theta\\
y=\sin \theta
\end{array}
\right.
これをGameMakerで実現する場合は,以下のようになります.細かい数字は適当です.

//step event
x = xstart + 4 * theta
y = ystart + 32 * sin(degtorad(theta) * 6)
theta += 1

こうすると,(xstart, ystart)から右方向にsinカーブを描きながら進んでいくオブジェクトが完成します.4とか6とかは適当です.


他にも媒介変数表示はいろいろあるので,動かしたいものがあれば調べてみましょう.
ここからは傾ける方法について書いていきます.

回転に使う式

まず,一般的な2次元空間についての話をします.
2次元空間上の点(x, y)を原点の周りに角θだけ回転させた点を(x', y')とすると,
\left \{
\begin{array}{}
x'=x\cos\phi -y\sin\phi\\
y'=x\sin\phi +y\cos\phi
\end{array}
\right.
と表されます.これについても「回転移動の1次変換」とかで調べればわかります.
見た目のイメージでいうとこんな感じ.

f:id:toki_0177:20190206140805p:plain:w200
回転移動のイメージ


ここで,図を見ればわかると思いますがGameMakerと座標軸の取り方が異なっているので,実装する場合はそこを考慮して書く必要があります.
GameMakerの座標軸はy軸が下向きが正となっていますが,角度の取り方は反時計回り方向が正となっているので,先ほどの回転に使う式を
\left \{
\begin{array}{}
x'=x\cos (-\phi) -y\sin (-\phi)\\
y'=x\sin (-\phi) +y\cos (-\phi)
\end{array}
\right.
のように変更する必要があります.

sinカーブを傾ける

ここから,GameMakerでの実装について書いていきます.
自機狙いの弾をsinカーブで飛ばすとすると,createイベントとstepイベントに以下のようなコードを書けばいいです.

//create event
phi = point_direction(x, y, player.x, player.y)
//step event
x_tmp = 4 * theta
y_tmp = 32 * sin(degtorad(theta) * 6)

x = xstart + x_tmp * cos(-degtorad(phi)) - y_tmp * sin(-degtorad(phi))
y = ystart + x_tmp * sin(-degtorad(phi)) + y_tmp * cos(-degtorad(phi))

theta += 1

ちなみに,x_tmp,y_tmpに使いたい媒介変数表示を入れることでsinカーブ以外にも対応させることができます.
これで実装は完了です.

アイワナ:ボスの基礎を作成する

この記事では主に「無敵時間の作成」とか「当たり判定の調整」とかとかそういう「攻撃パターン以外」の部分について書きます.
攻撃パターンとかについてはまたどこかで書くかもしれません.
まずは適当なボスのスプライトを作るところから.

ボスの当たり判定を作成する

spriteを作成する段階でボスの当たり判定の調整をしていきます.
「ボスにオーラっぽいの付けたくてGlowってエフェクトをかけたけど,オーラ部分には当たり判定付けたくない!」みたいなことってありますよね.
そういう時に使えるのがこの「Modify Mask」という機能です.

f:id:toki_0177:20190121210903p:plain
作成したスプライトの例とModify Maskボタン
ここをクリックして開いたウィンドウでいろんな数字をいじって自分の望んだ当たり判定になるように調整しましょう.
今回の場合は透過度でいい感じに分けられそうなので,「Alpha Tolerance」の数値をいじります.
128ぐらいにしてあげると図の左だった当たり判定が右になります.
今回はいじりませんでしたが,「Bounding Box」をいじると座標で当たり判定の有無を分けられたりもします.これも便利.
f:id:toki_0177:20190121211723p:plain
Modify Maskの画面

適当なボスのオブジェクトを作る

さっき作ったspriteを使用して適当なボスのオブジェクトを作ります.
ParentをplayerKillerにするとか,攻撃パターン作るとか.
無敵時間以外については今回は触れません.
次からいよいよ無敵時間についてです.

無敵時間を作る

結論から言うと「Collision bullet」イベントと「alarm[11]」に以下のコードをそれぞれ追加すればいい感じになります.

//collision bullet event
with(other)instance_destroy()
sound_play(sndBossHit)

if(muteki == 0){
  muteki = 1
  hp -= 1
  image_blend = make_color_rgb(128, 128, 128)
  alarm[11] = 60
}
//alarm[11]
muteki = 0
image_blend = -1

ここからは簡単にコードの解説を行っていきましょう.
Collision bullet
1行目:ヒットした弾を破壊する(複数回当たるのを防ぐ)
2行目:ヒット音を鳴らす
4行目:無敵時間中かどうかの判定
5行目:無敵時間中であるかどうかを示すフラグをオン
6行目:ボスのHPを減らす処理
7行目:ボスの色を変える処理(無敵時間であることをわかりやすく:詳しくは後述)
8行目:ボスの無敵時間解除を60ステップ後に設定


alarm[11]
1行目:無敵時間中であるかどうかを示すフラグをオフ
2行目:ボスの色をもとに戻す処理

image_blendとは?

7行目とかで使っているimage_blendの部分について詳しく説明していきます.
この変数はなかなか面白い変数で,簡単に言うとspriteに指定した色を重ね合わせることができる機能です.
(この機能に最近気づいたのでこの記事を書こうと思いました.)
(気づく前までは無敵時間用に色を変えたspriteを作ってましたが,複数スプライトある時にめんどくさいなあと思って色々調べてこの方法を見つけました.)
この機能の少し特殊なこととしては,白(R:255, G:255, B:255)は透過度100,黒(R0:, G:0, B:0)が透過度0になることです(たぶん).
この特徴を考えると,さっきの処理は透過度50でsprite全体を黒く塗るという処理です.
一定時間で元の色に戻り,無敵状態も解除されます.

f:id:toki_0177:20190121235651p:plain
無敵時間中の見た目
試していないからわからないけどmake_color_hsv()を使えば色相(H)と彩度(S)で色を決められて,明度(V)で透明度を決められるのかもしれないとか思ったり.

昔作ったアイワナを紹介する

ブログ始めた記念に昔作ったアイワナを載せておきます.
バランスとか今見ると何とも言えない感じですが修正するのも面倒なのでそのままです.
長さ的には中編ぐらいの感じ.
針メインですがボスとかミニゲームとかあります.
遊んでやってください.
↓DLはこちらから↓
I wanna see three years

f:id:toki_0177:20190120110837p:plain
タイトル画面

アイワナ:ポーズ画面を作る

それっぽい記事が見当たらなかったので,show_message()を使わないポーズ画面の作成について書こうと思います.
※ここで書く方法は「i wanna be the engine ErunatyanEdition」で使われているものと同じです.
※この記事では「i wanna be the engine yuuutu edition ver2.16」にポーズ画面を実装します.


この方法で作れるポーズ画面のイメージはこんな感じ↓

f:id:toki_0177:20190118121108p:plain
作成するポーズ画面のイメージ
ここからは実際に作り方を説明していきます.

ポーズ画面に表示する文字を作成する

drawイベントで「Pause」とかの文字を描画するのでも問題ありませんが,ちょっと装飾とかを加えるといい感じになるのでspriteに追加していきます.名前は「sprPause」とかで.

f:id:toki_0177:20190118110057p:plain
spriteにポーズ画面に表示したい文字を作成

ポーズ画面を描画するオブジェクトを作成する

オブジェクトを作成し,名前を「Pause」とかにします.
「Pause」のVisibleにチェックを入れ,Depthは-100ぐらいに.

ここから具体的に描画するものを設定していきます.
ポーズ中であることがわかるように画面全体を少し暗くする処理と,さっき作成した文字を表示する命令を「draw」イベントに書いていきます.

//draw event
draw_set_color(c_black);
draw_set_alpha(0.4);
draw_rectangle(x, y, x+800, y+608, false);
draw_set_alpha(1);

draw_sprite(sprPause, 0, x+400, y+304);

ポーズ状態にするためのオブジェクトを作成する

オブジェクトを作成し,名前を「objPause」とかにします.
Visibleとかの設定はそのままでいいです.
その後,「Create」イベントと「Step」イベントに以下のコードを書きます.

//create event
is_pause = false;
//step event
if(is_pause == true){
  while (1) {
    if (keyboard_check_pressed(vk_anykey)) {
      is_pause = false;
      with (Pause) instance_destroy();
      break;
    }
    sleep(10);
  }
}else{
  if (keyboard_check_pressed(global.pausebutton)) {
    is_pause = true;
    instance_create(view_xview, view_yview, Pause);
  }
}

作成したポーズ機能を使えるようにする

まず,オブジェクト「Player」の「Step」イベントに以下のコードを追加します.このif文に条件を追加したりすれば特定のルームではポーズ機能を使えなくするみたいなこともできます.

//step event
if(global.Pause_Message == 1 && !instance_exists(objPause)){
  instance_create(0, 0, objPause);
}


次に,「script」内の「SetGlobalOption」に,以下のコードを追加します.

//script SetGlobalOption
global.pausebutton = ord('P');

最後に,オブジェクト「world」の「press P-key」イベントを削除します.
これで完成です!お疲れさまでした.

numpy.ndarray.copy()が遅かったという話

作成しているプログラムの実行が遅かったのでcProfileを使って調べてみました.

その結果,実行時間の3分の2程度をnumpy.ndarray.copy()が占めていたので代わりの方法を書いておきます.


こんな感じのコードを

def hoge(num, array):
    for i in range(num):
        tmp = array.copy()

こうすることでかなり早くなりました.

def hoge(num, array):
    tmp = array.copy()
    for i in range(num):
        tmp[...] = array

オブジェクトのコピーをしなくなるから早いとかなのかな.