データ収集からAI開発、Webアプリ公開まで全てPythonで済ませた話（4.Webアプリ開発編）

# はじめに
今回の記事はシリーズ物です。他の記事はこちら↓

[0.設計編(キーを判別するAIとは？)](https://qiita.com/hanata/items/38163b6dddbc2ec8812e)
[1.データ収集(クローリング)](https://qiita.com/hanata/items/cbc6cc377321a2880f5b)
[2.データ整形(スクレイピング)](https://qiita.com/hanata/items/692f98ee83295614599f)
[3.機械学習を用いたAIの開発](https://qiita.com/hanata/items/d1fb6241cccf405bbc58)
[4.Djangoを用いたWebアプリ開発](https://qiita.com/hanata/items/968040a366c9bb1dbc93)←この記事です。

このシリーズもやっとこさ最終回です。色々な人に使ってもらえるよう、前回作ったモデルを搭載したWebアプリを作ります。
コードはGitHubにて公開しております。
https://github.com/hatena-hanata/

完成したWebアプリはコチラ
https://kja-app.herokuapp.com/

# 設計
デプロイで躓かないようにするため、超シンプル設計にしています。今回はDBを使用せず、modelも一切作りません。
![10411.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405491/04b07f44-e119-c6f7-651b-5ae42b171c41.jpeg)

機械学習モデルは静的ファイルとして、staticに格納しておきます。予測値を文字列に戻すための、ラベルエンコーダも一緒に格納します。


作る画面は以下の通り。

1. U-fretの楽曲URLを入力する画面(`InputUrlView`)
2. 結果を表示する画面(`ResultView`)

2つだけです。 Viewごとに細かい機能の説明をしていきます。

## InputUrlView
`FormView`です。URLを入力するボックスだけ用意します。`forms.py`で少しだけカスタムしたフォームを作成します。
スクレイピングやキーの予測はResultViewで行うので、入力されたURLをResultViewに渡す必要があります。 ViewからViewへ変数を渡す方法が分からなかったため、URLをテキストファイルに出力します。

```python
class InputUrlView(FormView):
    template_name = 'kj_ai/input_url.html'
    form_class = InputUrlForm

    # バリデーションを通過すると、入力されたURLをテキストファイルに書き込む
    def form_valid(self, form):
        url = form.cleaned_data['url_str']
        url_dir = os.path.join(BASE_DIR, 'static/model/input_data.txt')
        with open(url_dir, 'w') as f:
            f.write(url)
        return redirect('kj_ai:result')
```


### InputUrlForm
bootstrapのデザインを反映させるために、初期化メソッドでform-controlクラスを付与しています。

入力されたURLのバリデーションチェックはここに書きます。今回はUfretの曲ページのURLだけを受け付けるようにします。
正規表現でチェックしているのですが、これだけだとパターンは合っているものの、末尾の数字がテキトーで曲データが存在しない場合も、バリデーションを通過してしまいます。そのため、一度入力されたURLでスクレイピングしてみて、曲が存在するかどうかを確認しています。

```python
class InputUrlForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super(InputUrlForm, self).__init__(*args, **kwargs)
        # bootstrapを反映させるために、form-controlを付与
        for field in self.fields.values():
            field.widget.attrs["class"] = "form-control"

    url_str = forms.CharField(max_length=60, required=True, label='URL')

    # バリデーション
    def clean_url_str(self):
        input_url = self.cleaned_data['url_str']
        # URLのパターンがU-fretの曲データのURLではない
        pattern = 'https:\/\/www\.ufret\.jp\/song\.php\?data=[0-9]+'
        if re.search(pattern, input_url) is None:
            raise forms.ValidationError('URLが正しくありません')

        url = re.search(pattern, input_url).group()

        # URL末尾の番号によっては曲が存在しないので、スクレイピングして確認
        r = requests.get(url)
        soup = BeautifulSoup(r.content, 'html.parser')
        if soup.find('title').text.split('/')[0] == ' ':
            raise forms.ValidationError('URLが正しくありません')

        return url
```


## ResultView
結果を表示するViewですが、内部では色々やっています。

1. テキストファイルからURLを読み込んでスクレイピングを行って、予測に使う説明変数となるデータフレームを作成
2. キーの予測をして、ラベルエンコーダで予測値を文字列に戻す
3. 結果をテンプレートに渡す

こんな感じです。

```python
class ResultView(TemplateView):
    template_name = 'kj_ai/result.html'

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super().get_context_data(**kwargs)

        # 入力されたURLを読み込む
        url_dir = os.path.join(BASE_DIR, 'static/model/input_data.txt')
        with open(url_dir, 'r') as f:
            url = f.read()
        # urlから曲のコードカウントしたdfと曲名を抽出
        df, song_name = scraping(url)

        # urlを記載したファイルを削除
        os.remove(url_dir)

        # 学習済みモデルとラベルエンコーダーの読み込み
        model_pkl_dir = os.path.join(BASE_DIR, 'static/model/model.pkl')
        le_pkl_dir = os.path.join(BASE_DIR, 'static/model/le.pkl')
        loaded_model = pickle.load(open(model_pkl_dir, 'rb'))
        le = pickle.load(open(le_pkl_dir, 'rb'))

        # 予測して、結果を文字列に変換
        pred_y_value = loaded_model.predict(df)
        pred_y_label = le.inverse_transform(pred_y_value)[0]
        if pred_y_label.split('_')[-1] == 'Major':
            answer = pred_y_label.split('_')[0]
        else:
            answer = pred_y_label.split('_')[0] + 'm'

        # htmlに結果を渡す
        context['answer'] = answer
        context['song_name'] = song_name
        return context
```

スクレイピングのコードはコチラです。

```python
def func(url):
    r = requests.get(url)
    soup = BeautifulSoup(r.content, 'html.parser')
    # 曲名取得
    song_name = soup.find('title').text.split('ギ')[0]
    song_name = song_name[:len(song_name) - 1]

    # Songインスタンス作成
    song = Song(song_name, Chord('C'))

    # コードを読み取る
    chord_lst = soup.find_all('rt')
    for c in chord_lst:
        chord_str = c.text
        if c.text[0] == 'N':
            continue
        chord = Chord(chord_str)
        song.append_chord(chord)

    # dfに変換して、説明変数部分だけ抽出。
    df = song.to_DataFrame()
    df = df[df.columns[2:]]
    for col in df.columns:
        df[col] = df[col].astype(float)

    return df, song_name
```


# デプロイ
いよいよデプロイ！全て無料で済ませたいので、herokuにデプロイします。
以下のサイトを大いに参考にしました。ほとんど下のサイトのコピペになってしまうので、詳しい手順はぜひ下のサイトを御覧ください。

[[Django] Heroku デプロイ方法 2018年版](https://qiita.com/okoppe8/items/76cdb202eb15aab566d1)
[Django + Heroku + WhiteNoise + AWS S3 によるWebアプリのデプロイ](https://qiita.com/knakajima3027/items/f8d10e3066fbcde117d3)

学習済みのモデルやラベルエンコーダは、静的ファイルとして全てプロジェクトフォルダ直下の`static/model`に`.pkl`として格納しております。詳しいディレクトリ構造に興味がありましたら、GitHubの方をご確認ください。


# 完成
https://kja-app.herokuapp.com/

Webアプリをリリースするのは初めてなので、とても嬉しいです。我が子のように可愛がろうと思います。

# 次回やりたいこと
今回の開発を進めていく上で、新たなアイデアが出てきました。

## 曲中の転調を検出するAIの開発
僕が作ったキーを判別するAIの最大の敵は、転調する曲です。転調する曲に対しても一つしかキーを出力できないため、予測が難しくなります。
転調を検出するAIが作れれば、キーを複数出力するAIが作れるかもしれません。
ただ、転調するかどうかのラベルが記載されているコード譜掲載サイトは無いので、教師なし学習になりそうです。教師なし学習に関しては知識不足なので、勉強しようと思います。


# さいごに
ここまで読んでくださった皆様、本当にありがとうございます。少しでも役に立てば幸いです。質問等あれば気軽にお願いします。

今回のAIの精度検証ですが、どうしても人力での確認が必要です。キー判別に自信のある方、実際に使ってみた感想を教えていただきたいです！笑  よろしくお願いします〜