【19日目】簡易機械学習ソフトの完成【Pythonで音楽系人工知能を作るまでの日記】

プログラミング

2023年6月27日

おはようございます。
昨日の続きをしようと思う。

とりあえず、機械学習モデルはK-近傍法が使いやすかったので、今回はK-近傍法で作成する(後々K-近傍法ではダメなことが分かったので、最終的にはランダムフォレストで実装)

フロントエンドの部分なんだけど、Pythonの標準ライブラリにデスクトップアプリを作成できるものがあるらしい。また、外部のライブラリも豊富っぽいのでPythonだけでいけそう。初めて習った言語がPythonで本当に良かった。

最終的な理想としては

  • 特定のフォルダを作ってそこに音源を一括で入れると、全音源にラベル付けをしてくれるソフト。
  • 検索機能でそのラベルを使って検索できる。例えば、FXを最優先、2番目をPadにすると、FXが1番目高い音源かつ、Padが高い音源? 検索機能はもうちょい詰めれそう。今回は6種類だからちょっと微妙なんだけど、将来的にvoice&male &trapみたいな感じで検索して理想のものが出てきたら便利だと思う。
  • 他に、入れた音源の解釈をAIがしてくれる機能があると面白いと思う。入れたのはFXだったとしても、AIがStabって判断したりして、ユーザーが「確かにStabにも使えるかもなぁ」ってなると理想。

しかし、今回はお試し&時間があまりないので、入れた音源をAIが解釈してくれる機能だけを搭載しようと思う。

今回実装するものの理想は

  • Windowsで動く
  • Wav形式とmp3形式に対応
  • 音源をドラック&ドロップで特定の範囲に入れることで動く

ってところかいな。

ドラック&ドロップで音源を入れれるとことかめっちゃ難しそう。

とりあえずやってみないと何が難しいのかがわからないのでやってみる。実装の部分は ChatGPTの助けを大いに借りようと思うんだけど、いけんのかな。とりあえず、やってみよう。

実装

私が密かに参考しているFrieve-Aさんというつよつよエンジニアの方は、pygameというゲーム向きのPythonライブラリで音楽系ツールを制作していたんんだけど、ChatGPTに聞いたところ、「最初はtkinterという標準ライブラリを使った方がいいんじゃないすかね?」と言われたので、そうする。tkinterの拡張的外部ライブラリである、tkinterdnd2というをの使うと、簡単にドラック&ドロップで対象のパスを取得できるようになるらしいので、今回はこちらも用いて作成する。

恐らく、見た目を重視するとpygameの方がいいんだけど、今回はプロトタイプなのを重視して、効率極振りtkinterで作る。

実は、ここまでmacでやってたんだけど、macだとtkinterdnd2をインストールする際、tkdndっていうのを別にインストールする必要があるらしくて面倒くさそうだったので、ここからはwindowsで作る。
最近GithubとかGitの使い方も分かってきて、プライベートレポジトリからClone出来るようになった。えらい。

ChatGPTに生成してもらった以下のコードでGUIっぽい所にD&Dをするとファイルパスを取得できるようになった。

※詳しい最終的なコードはgithubに上げる予定なのでそちらを

import tkinter as tk
from tkinterdnd2 import DND_FILES, TkinterDnD


def drop(event):
    filepath = event.data
    print(f'{filepath} was dropped.')
    # ここでscikit-learnを用いた音源分析などを実装します。


root = TkinterDnD.Tk()
root.withdraw()  # tkinterのウィンドウを一時的に隠す


frame = tk.Frame(root, name='drag_and_drop_frame', width=300, height=300)
frame.pack()


# フレームにドロップ操作をバインド
frame.drop_target_register(DND_FILES)
frame.dnd_bind('<<Drop>>', drop)


root.update()
root.deiconify()  # tkinterのウィンドウを再表示
root.mainloop()

ここから、学習させたK-近傍法を追加する。

pickleを使って学習結果のモデルを保存する方法は知ってたんだけど、ChatGPTがjoblibを提案してきたのでそちらでやってみる。
pickleは標準モジュールで、joblibは外部モジュールらしく、joblibの方が処理が早いんだそう。

以下のようにモデルをGoggleDriveに保存して、ダウンロード。

# モデルを保存
joblib.dump(model, '/content/drive/MyDrive/…')

ここからは、ChatGPTじゃまともなコードを生成できなかったので色々調べて頑張る。

結果、日本語orスペースが入ってるとパスが適切に変換されない問題が発生。
日本語が入ったり、空白が入ったパスを読み込むと→{C:/Desktop/音楽/素材\\素材1.mp3}みたいに{}が付いたり\\になったりしてしまう。

無理やり直してもいいんだけど、根本的解決にならないのがちょっと。

うーーん。とりあえず今回はしょうがないのでreplaceで以下みたいに変換する方針にする。

filepath = file_path.strip("{}")  # 根本的解決にはならない修正
filepath = filepath.replace("/", "\\")  # 根本的解決にはならない修正

これでパス問題は無理やり解決。

よし。
とりあえず、ドラック&ドロップで予測するところまで出来た。
以下がprintしたその結果。

それぞれ、ヨッシーの「でっていう」っていう声と、艦これの島風の「ウッ!」っていう声を入れてみた。

どちらもBassに分類されてる……なんでだ?

この結果を見て、スゴイ今更思ったことなんだけど、K-近傍法使うなら各学習データは等分じゃないと多いやつに偏るよねこれ。超盲点だった。
だからK-近傍法の正解率が高かったという説は結構濃厚。
もし、K-近傍法使うなら幅広いデータを均等に学習させないと、偏ってしまうことに本気で気づいてなかった。

昨日の記事、修正しときます。

ちょっと納得いかないので、モデルをランダムフォレストにしてやり直してみる。

おー! 結構いい感じに予想してくれるぞこっちは!

でっていうは確かにFXっぽいし、ウッ!はStabっぽい。ランダムフォレストやるなぁ。

これであとは見た目を整えれば完成か? ちょっと頑張ってみる。

tkinterで画像を扱うところがあったんだけど、画像の扱いはこの人の記事がすごく分かりやすかった。特に、indexで画像の状態を処理するところらへんが天才。

完成!

よし! できたぞ! 勉強記録見たら、今日はもう合計8時間機械学習弄ってたぞ!

お試しに何か音源を入れてみよう。
魔王魂にベースの単音があったので、これを入れてみる。

①まず、起動したらランダムフォレストちゃんに音源を食べさせます。
日本人は何でも擬人化させたいんです。文句は受け付けません。

②ドラック&ドロップで食べさせて、wav形式またはmp3形式であれば準備完了となります。

③「予測をしてもらう!」ボタンを押すと!

④こんな感じで予測してくれます! かわいい!

因みに、今回のBassは内部的に[0.3685     0.085      0.14       0.         0.09583333 0.31066667]と予測されていて、順に[Bass,FX,Lead,Pad,Pluck,Stab]なので、結構ギリギリベースって判断した感じ。
学習データが少ないし、深層学習とかではないので完璧な本質的判断をしてくれるわけではないんだけど、悪くはないかな。

あとはexe化して配布できれば終わり!

exe化

exe化はpyinstallerなるもので出来るらしい。手段を見るとあまりにも簡単だからちょっと疑ってる。

……

うん、簡単じゃなかった。
これ、文章だから一瞬なんだけど、マジで3時間くらいエラーと戦ってた。

ぁぁあ…ああぁ。知識がない+情報がないので本当に大変だった。


結論から言うと5つの壁がある

  • 1つ目が相対パスの問題
    こんなのは屁でもないんだけど、一応昔の私なら引っかかる可能性はあるので入れといた。pythonファイル内でパスを指定するとき、相対パスにしないと環境依存になるよ。かつ、相対パスにするならexeファイルを実行する場所が大切になるよっていう話。ファイルの階層を調整してね。
    「FileNotFoundError: [Errno 2] No such file or directory:」というエラーが出たらこれ。
  • 2つ目がtkinterdnd2の壁
    なんか、pyinstallerでexeファイルを作成して、実行すると

「RuntimeError: Unable to load tkdnd library.」っていうエラーが。

これはpyinstallerを実行するとき–collect-data tkinterdnd2を付けるとエラーがなくなった。余り深い理由はわからないが、collect-dataを付けるとパッケージ化があーだこーだするらしい。

  • 3つ目がlibrosaの壁

librosaでも同じエラーが出た。同じなので「–collect-data librosa」を付けて解決。
一応参考程度に、この時点でのpyinstallerの実行コマンドは「pyinstaller ○○○.py –onefile –noconsole –collect-data tkinterdnd2 –collect-data librosa」という感じ。
デバッグするなら–noconsoleを消した方がいい。

  • 4つ目がsklearnの壁

下にもある通り、sklearn.ensemble_forestをimportしとかなきゃいけなかったっぽい。
「from sklearn.ensemble import RandomForestClassifier」で。

  • 5つ目が返ってきたlibrosaの壁

ここまでは1時間もかからなかったと思うんだけど、ここから2時間かかった。

結論から言うと、librosaでエラーが起きててボタンを押しても何も返答がなかった。
「pip uninstall librosa」→「pip install librosa==0.9.2」でlibrosaのバージョンを下げるといけた。

librosaの最新バージョンだとLazy_loaderというのが使われていて、それとpyinstallerの相性が悪い。
一応、Lazy_loader有りでやる方法もあるんだけど、ちょっと知見が無さ過ぎて私には無理だった。というか、情報が本当に無い。
とりあえず、0.9.2にすればLazy_loaderは使われてないので行けるという感じ。

これでようやく、しっかり起動、作動するexeファイルが出来たんだけど……

容量でかくね?

12万KB=120MBなので、かなりある。
これは、exe化するときpipにインストールしているモジュールがすべて入ってしまうかららしい。

これを回避するために、仮想化というのを行って必要なものだけインストールしてexe化するという操作がいるっぽい。

仮想化か……正直もう1日経って次の日になっているので疲れてるんだけど、ここまで来たらやろう。
ついでに、アイコン設定もできるらしいのでアイコンもやる。

アイコンはexe化するとき「–icon=アイコン名.ico」をつければいけて、アイコン作成サイトとかでリサイズするのが一番簡単っぽい。
アイコンキャッシュの影響でアイコンが変わんなかった時は再起動またはWindows+R→ie4uinit.exe -showでいける。

仮想環境は、仮想環境についての記事と、それをどう使うかの記事を見ればできた。仮想環境って聞くと苦手意識があるんだけど、そんなに難しくは無かった。

よし!完成!

容量は……

うん。私は何も見てない。うん。3000KBも減った!? やったね!

アイコンはStableDifusionさんに作成してもらった。ありがたや。

完成!!!!

容量はなんと驚異の250MB!! そこらへんのインディーゲームよりあるぞこれ。

でも、一応簡易的ながら理想的なものが作れて満足。

配布やら何やらはreadmeとか書いた方がいいと思うので明日というかもう今日だけど……起きたらやる。

おやすみなさい