2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

データ収集からAI開発、Webアプリ公開まで全てPythonで済ませた話(2.データ整形編)

Last updated at Posted at 2019-08-25

はじめに

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

0.設計編(キーを判別するAIとは?)
1.データ収集(クローリング)
2.データ整形(スクレイピング)←この記事です。
3.機械学習を用いたAIの開発
4.Djangoを用いたWebアプリ開発

この記事では、クローリングしたhtmlファイルから、教師データを作成するまでの過程を書いていきます。またコードに関するデータを扱いやすくするため、簡単なクラス設計を行いました。

音に関するクラスを作る

htmlファイルからコードの文字列を抜き出して、コードの出現頻度をカウントしていくのですが、いくつかやらなければいけないことがあります。

  • コードの文字列(CmやDm7)から、余計な情報を省く。
    コードの出現頻度をカウントしていくのですが、7thやdimなどの細かい部分まで考慮してカウントしていくと、説明変数が多くなるし、スパースなデータ(0が多いデータ)になってしまいます。そのため今回は、「ルート音」と「メジャーコードかマイナーコードかのフラグ」を元にカウントしていきます。

  • J-Total Musicに掲載されているコードは、ギターで弾きやすくするために、転調された状態でコードが記載されている場合がある。
    そのままカウントしてしまうと駄目なので、原曲キーに転調させてカウントする必要があります。幸い、J-Total Musicでは「どれだけキーをずらせば原曲キーに戻るか」という値が書いてあるので、それを元に転調しながら、コードをカウントしていきます。

もちろん「情報を省く」ことや「コードを転調させる」メソッドを書いていけばよいのですが、見通しが悪くなるので簡単なクラスを作ってみようと思います。

クラスたち

Noteクラス(音名に関するクラス)

class Note:
    name_lst = ['C', 'C#/D♭', 'D', 'D#/E♭', 'E', 'F', 'F#/G♭', 'G', 'G#/A♭', 'A', 'A#/B♭', 'B']

    def __init__(self, s):
        if len(s) == 1:
            self.name = s
        else:
            for index, c in enumerate(Note.name_lst):
                if len(c) != 1:
                    c_split = c.split('/')
                    if s in c_split:
                        self.name = c

    def transpose(self, step):
        if step == 0:
            return self
        prev_index = Note.name_lst.index(self.name)
        now_index = (prev_index + step) % 12
        self.name = Note.name_lst[now_index]
        return self

    def __str__(self):
        return self.name

name_lstというクラス変数は、以前の記事で書いた12種類の音のリストになっています。
transposeメソッドを使用することで、音を指定した幅だけ上下させることができます。
初期化メソッドですが、今回はエラー処理の対応は書いておりません。正しい値が入力されている前提で書かれています。

Chordクラス(コードに関するクラス)

class Chord:
    # s = 'Dm7' など受け取る
    def __init__(self, s):
        # コードのルート音
        if len(s) == 1:
            self.root = Note(s)
        else:
            if s[1] == '#' or s[1] == '':
                self.root = Note(s[0:2])
            else:
                self.root = Note(s[0])

        # コードがメジャーかマイナーか
        if 'm' in s:
            self.is_major = False
        else:
            self.is_major = True

    def __str__(self):
        if self.is_major:
            return '{}_Major'.format(self.root.name)
        else:
            return '{}_minor'.format(self.root.name)

コードに関する文字列を受け取って、インスタンスが初期化されます。
上で述べたように、今回は「ルート音」と「メジャーコードかマイナーコードか」の情報だけ抽出します。

Songクラス(曲に関するクラス)

class Song:
    def __init__(self, name, key):
        """
        コンストラクタ
        :param name: str
        :param key: Chordのインスタンス
        """
        # 曲名
        self.name = name
        self.chord_count_dict = {
            'name': name,
            'key' : key,
            'C_Major': 0,
            'C_minor': 0,
            'C#/D♭_Major': 0,
            'C#/D♭_minor': 0,
            'D_Major': 0,
            'D_minor': 0,
            'D#/E♭_Major': 0,
            'D#/E♭_minor': 0,
            'E_Major': 0,
            'E_minor': 0,
            'F_Major': 0,
            'F_minor': 0,
            'F#/G♭_Major': 0,
            'F#/G♭_minor': 0,
            'G_Major': 0,
            'G_minor': 0,
            'G#/A♭_Major': 0,
            'G#/A♭_minor': 0,
            'A_Major': 0,
            'A_minor': 0,
            'A#/B♭_Major': 0,
            'A#/B♭_minor': 0,
            'B_Major': 0,
            'B_minor': 0,
        }
        self.original_key = key

    def append_chord(self, c):
        self.chord_count_dict[str(c)] += 1

    def to_DataFrame(self):
        return pd.DataFrame.from_dict(self.chord_count_dict, orient='index').T

インスタンス変数で、曲名・原曲キー・コード出現回数をあわせた辞書を持ちます。
コードのカウントは append_chordメソッドで行います。
最終的にデータフレームで出力するためにto_DataFrameメソッドを用意しています。インスタンス変数の辞書をdfに変換してreturnするだけの簡単なメソッドです。

スクレイピング

以上のクラスたちを使用しながら、必要な情報を抜き出してデータフレームを作っていきます。
流れとしては

  1. htmlファイルからキーに関する情報を取得、取得できない場合スキップ
    たまにキーの情報が書いてない場合があります。そういう曲は迷いなくスキップしていきます。

  2. コードが書いてあるタグからコードを文字列で取得。Chordインスタンスを作成しながら、Songインスタンスにappendする

という感じになっております。

コード

# ライブラリ
import glob
import os
from bs4 import BeautifulSoup
import re
import pandas as pd
import codecs
from tqdm import tqdm

# 自作クラスのインポート
from music_class import Note, Chord, Song


# htmlからコードを抽出するときの正規表現パターン
CHORD_PATTERN = r'[A-G]{1}[#,♭]?[m]?'


# htmlからキーに関する情報を取得
def getKeyInfo(soup):
    info = soup.find_all('font', size='3')
    song_info = None
    
    # key情報が曲ごとにまちまちなので、調べる
    for i in info:
        if 'Original' in i.text:
            song_info = i.text
    # key情報がなかったらスキップ
    if song_info is None:
        return None, None, True

    # transpose幅を求める
    if '半音' in song_info:
        transpose_step = -1
    else:
        try:
            transpose_step = int(re.sub(r'\D', '', song_info))
        except:
            return None, None, True
    # original keyを文字列で取得
    try:
        original_key_str = re.search(CHORD_PATTERN, song_info).group()
    except:
        return None, None, True

    return original_key_str, transpose_step, False


def main():
    # 全htmlファイルを取得
    html_files = [p for p in glob.glob('html/**', recursive=True) if os.path.isfile(p)]

    # 出力用df
    ans_df = pd.DataFrame()

    # ファイルごと処理
    for html in tqdm(html_files):

        # ファイルを開く
        with codecs.open(html, 'r', 'shift-jis', 'ignore') as f:
            soup = BeautifulSoup(f, 'html.parser')
            song_name = soup.find('title').text.split('/')[0] # 曲名取得

            # keyや転調幅がいくつか取得する
            original_key_str, transpose_step, continue_flg = getKeyInfo(soup)

            # キーの情報が取得できない場合、スキップする
            if continue_flg:
                print('{} はデータに不備があり、教師データとして使用できません'.format(song_name))
                continue

            # 曲インスタンスを作成
            song = Song(song_name, Chord(original_key_str))

            # コードが書いてあるタグをすべて取得
            chord_lst = soup.find_all('a', href=re.compile("^JavaScript:jump_1"))

            for c in chord_lst:
                # 文字列のコードからコードインスタンスを作成
                try:
                    chord_str = re.search(CHORD_PATTERN, c.text).group()
                except:
                    continue
                # Chordインスタンスを作成
                chord = Chord(chord_str)

                # コードをtransposeしてから、songインスタンスにappendして、コードをカウントする
                chord.root = chord.root.transpose(transpose_step)
                song.append_chord(chord)

            # df化したのち、出力用dfにappendする
            song_df = song.to_DataFrame()
            ans_df = ans_df.append(song_df)

    ans_df.to_csv('train.csv', encoding='shift-jis', index=False)



main()

教師データ完成!

こんなCSVが出来上がります。
image.png
小さくと見づらいですが、ちゃんとできています!

データを眺めてみる

少しデータを覗いてみます。今回集めた曲たちの中で、各キーの曲数をグラフ化してみます。
image.png
1位はG(ト長調)でした。2位はC(ハ長調)ですね。その後も長調が続いていきます。今回集めたデータの中では、短調は全体的に少なめな傾向があります。
1位のGと最下位のG#/A♭mでは10倍近くの差があります。びっくりですね。

次はダイアトニックコードの出現割合を見てみましょう。キーCの曲で確認してみます。
image.png
すごい見づらいのですが

一番下の層の青色は C
下から二番目の黄緑色は Dm
真ん中らへんの濃い青色は F
その上の灰色っぽいのは G
上のオレンジ色は Am

を表しています。キーCのダイアトニックコードはC,Dm,Em,F,G,Am,Bm7-5なので、たしかにダイアトニックコードが他のコードに比べて頻繁に使用されているのが分かると思います。

次の記事はこちら

いよいよ機械学習モデル作成に入っていきます!

3.xgboostを用いたAIの開発
4.Djangoを用いたWebアプリ開発

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?