[PYTHON] J'ai vectorisé l'accord de la chanson avec word2vec et je l'ai visualisé avec t-SNE

Aperçu

La chanson est composée d'accords appelés accords. L'ordre dans lequel ils sont arrangés est très important et change l'émotion de la chanson. Un bloc d'accords multiples est lu comme une progression d'accords, et il en existe un typique appelé [I-V-VIm-IIIm-IV-I-IV-V], par exemple. ** En termes d'ordre d'arrangement, si vous remplacez les chansons par des phrases et les accords par des mots, vous pouvez voir la corrélation entre les accords en les vectorisant avec word2vec et en les compressant en deux dimensions avec t-SNE. J'ai vérifié l'hypothèse de **. Je l'ai écrit obstinément, mais n'est-ce pas cool d'analyser des accords (accords) avec des accords ** (programmation)? Je vous serais reconnaissant de bien vouloir sympathiser avec ** </ font>.

(C'est complètement spéculation, mais il existe un chef-d'œuvre qui résume comment écrire du code lors de la programmation appelée code lisible, et je pense que c'est pourquoi la couverture est une note. .)

[Cliquez ici pour le code lisible] https://www.oreilly.co.jp/books/9784873115658/

Technologie principale

・ Grattage (sélénium == 3.141.0) ・ Word2Vec (gensim == 3.7.3) ・ T-SNE (scikit-learn == 0.20.3)

Connaissances préalables

・ Grammaire de base en python

・ Comprendre la progression du code (si vous ne comprenez pas, passez le chapitre 2)

Public cible

・ Les personnes qui connaissent le code et la progression du code ・ Les personnes qui souhaitent remplacer la progression du code par des chiffres romains

・ Les gens qui étudient le python ・ Les gens qui veulent savoir comment gratter ・ Les personnes intéressées par l'apprentissage automatique (traitement du langage naturel)

Structure du chapitre

Je vais l'écrire en trois chapitres.

** Chapitre 1: Collecte de données par grattage à l'aide de sélénium ** (environ 100 lignes) ** Chapitre 2: Remplacement de la progression du code par des chiffres romains ** (environ 150 lignes) ** Chapitre 3: Vectoriser le code avec word2vec et le montrer avec t-SNE ** (environ 50 lignes)

Si vous voulez faire référence au grattage par sélénium, veuillez consulter le chapitre 1, si vous êtes intéressé par la musique, veuillez consulter le chapitre 2, et si vous êtes intéressé par le type de résultat que word2vec apportera, veuillez consulter le chapitre 3.

Passons maintenant au contenu du code.

Chapitre 1: Collecte de données par grattage à l'aide de sélénium

La destination de collecte de données cette fois-ci est le site de U-FRET. Il y a beaucoup de données de chansons, de haute précision et de paroles, donc je pense que c'est un site que les gens qui jouent et parlent ont utilisé une fois.

[Cliquez ici pour U-FRET] https://www.ufret.jp/

Ici, spécifiez un artiste et créez un code qui produit la progression d'accords et les paroles de toutes les chansons en csv. (Déterminez ce que signifie le code à partir du contexte. Lol)

Je pense que Selenium ou Beautiful Soup sont célèbres pour gratter avec du python, Alors que Selenium spécifie le pilote et effectue réellement la transition d'écran pour acquérir l'élément, BeautiflSoup spécifie uniquement l'URL et acquiert l'élément. Je pense que BeautiflSoup est facile à écrire et à comprendre, mais comme il y a une limite à ce que je peux faire, les éléments ne peuvent pas être extraits à moins que ce ne soit Selenium, j'ai donc utilisé Selenium cette fois.

scraping.py


from time import sleep
from selenium import webdriver
import chromedriver_binary
import os
import csv
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.select import Select

#Entrée d'artiste
artist = "aiko"

#Sortie CSV du titre de la chanson
fdir = "chord_data/" + artist + "/"
os.makedirs(fdir,exist_ok=True)
#URL d'accès
TARGET_URL ='https://www.ufret.jp/search.php?key=' + artist


#Augmentation de la vitesse de démarrage du navigateur
options = webdriver.ChromeOptions()
options.add_argument('--user-agent=hogehoge')
#Lancer le navigateur
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(5)
driver.get(TARGET_URL)
url_list= []
urls = driver.find_elements_by_css_selector(".list-group-item.list-group-item-action")
for url in urls:
    text = url.text
    if (not "Code facile pour les débutants" in text) and (not "Vidéo Plus" in text):
        url = url.get_attribute("href")
        if type(url) is str:
            if "song." in url:
                url_list.append(url)

for url in url_list:
    #sleep(3)
    #driver.implicitly_wait(3)
    driver.get(url)
    sleep(5)
    #Passer à la clé d'origine
    elem = driver.find_element_by_name('keyselect')
    select = Select(elem)
    select.select_by_value('0')
    sleep(1)
    #Obtenir le titre de la chanson
    title_elem = driver.find_element_by_class_name('show_name')
    title = title_elem.text
    print(title)
    sleep(1)
    #Obtenir le code
    chord_list = []
    chord_elems = driver.find_elements_by_tag_name("rt")
    for chord in chord_elems:
        chord = chord.text
        chord_list.append(chord)

    #Obtenir les paroles
    lyric_list = []
    lyric_elems = driver.find_elements_by_class_name("chord")
    for lyric in lyric_elems:
        lyric = lyric.text.replace('\n',"")
        lyric_list.append(lyric)
    #Obtenez les paroles uniquement
    no_chord_lyric_list = []
    no_chord_lyric_elems = driver.find_elements_by_class_name("no-chord")
    for no_chord_lyric in no_chord_lyric_elems:
        no_chord_lyric = no_chord_lyric.text
        #Avancez uniquement les paroles de la liste des paroles pour qu'elles correspondent à la progression d'accords
        idx = lyric_list.index(no_chord_lyric)
        lyric_list.remove(no_chord_lyric)
        if idx==0:
            lyric_list[0] = no_chord_lyric + lyric_list[0]
        else:
            lyric_list[idx-1] += no_chord_lyric

    #Supprimer le code au début de chaque paroles et ne laisser que les paroles
    lyric_list = [lyric.replace(chord_list[idx],"") if chord_list[idx] in lyric else lyric for idx,lyric in enumerate(lyric_list)]
    
    #Sortie du résultat du scraping en csv
    fname = fdir + title + ".csv"
    with open(fname, "w", encoding="cp932") as f:
        writer = csv.writer(f)
        writer.writerow([])
        writer.writerow(chord_list)
        writer.writerow([])
        writer.writerow(lyric_list)

Ici, l'artiste est aiko. (Les résultats finaux incluent les résultats d'analyse d'autres artistes.)

Si vous utilisez mac, les outils de développement s'ouvriront avec l'option + commande + i, afin que vous puissiez savoir où se trouve l'élément spécifié.

L'acquisition en elle-même est facile car il suffit de définir la classe cible et la balise, mais les paroles sont un peu compliquées pour correspondre à la progression d'accords, mais il n'y a pas de problème même si vous ne comprenez pas.

Le plus important est de mettre du temps en sommeil pour ** ne pas mettre de charge sur le serveur d'U-FRET et prendre en compte le temps de traitement du chargement de la page **. Dans l'exemple ci-dessus, le serveur est accédé à des intervalles de 5 secondes. Cependant, même avec cela, parfois il ne parvient pas à obtenir l'élément, alors ajustez-le en le faisant plusieurs fois ou en augmentant le temps. (J'ai senti qu'implicitement_wait ne fonctionnait pas. Je vous serais reconnaissant si vous pouviez me faire savoir si vous avez des détails.)

À propos, le résultat de sortie ici est le suivant. スクリーンショット 2020-11-01 16.51.10.png

Le csv du nom du morceau est émis. De plus, la progression du code est sortie sur la deuxième ligne et les paroles correspondantes sont sorties sur la quatrième ligne. La première partie est une intro, donc aucune parole n'est jointe. Ne vous inquiétez pas ici car il remplira les lignes vides dans les deux prochains chapitres.

Chapitre 2: Remplacement de la progression du code par des nombres romains

Cela ne peut être compris que par quelqu'un qui est familier avec la musique dans une certaine mesure, donc si vous ne le comprenez pas, sautez-le. Si vous connaissez l'apprentissage automatique, vous pouvez reconnaître que ** la normalisation est effectuée sous forme de prétraitement de données **. Pour faire simple, considérez-le comme ** faisant de l'affichage absolu un affichage relatif afin que toutes les données puissent être traitées sur la même base **.

Pour expliquer un peu, il y a un son principal dans la chanson, et il est lu comme clé. Je suis sûr que certains d'entre vous ont déjà chanté avec la touche enfoncée parce que c'est cher même en karaoké, mais c'est à peu près tout. Par exemple, j'ai écrit la progression Kanon que j'ai expliquée au début comme [I-V-VIm-IIIm-IV-I-IV-V], mais je ne peux pas la jouer parce que je ne connais pas les accords réels. La progression du code écrit dans ce nombre romain est après la standardisation que nous voulons faire dans ce chapitre. En fait, c'est une progression comme [D-A-Bm-F # m-G-D-GA], qui est une progression Kanon avec clé = D. Lorsque le code D est I, le code A est V, et ainsi de suite, le nombre romain indique la position relative sur le clavier.

Maintenant, comment estimer la clé. ** Comparez les codes diatoniques des 12 types de clés avec la triade de tous les accords de la chanson, et trouvez la clé avec le taux de correspondance le plus élevé. Le jugement est fait par un algorithme appelé ** </ font>. Si vous connaissez la musique, vous savez ce que vous voulez dire, non? Lol

C'est donc un processus compliqué, mais jetons un coup d'œil au code.

scraping.py


import csv
import glob
import collections
import re
import os

artist = "aiko"
#Répertoire des données
fdir = "chord_data/" + artist + "/"
os.makedirs(fdir,exist_ok=True)
#Type de clé
key_list = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
#Chiffres romains
rome_list = ["Ⅰ","#I","Ⅱ","#Ⅱ","Ⅲ","Ⅳ","#Ⅳ","Ⅴ","#Ⅴ","Ⅵ","#Ⅵ","Ⅶ"]
#Code diatonique
dtnc_chord_list_num = ["Ⅰ","Ⅱm","Ⅲm","Ⅳ","Ⅴ","Ⅵm","Ⅶm7-5"]
#Tous tous à moitié tous à moitié
dtnc_step = [2,2,1,2,2,2,1]
#♭#Convertir en
r_dict = {'D♭':'C#', 'E♭':'D#', 'G♭':'F#','A♭':'G#','B♭':'A#'}
#Progression du code de chargement
flist = glob.glob(fdir+"*")

dtnc_chord_list_arr = []

for idx,key in enumerate(key_list):
    pos = idx
    dtnc_chord_list = []
    for num,step in enumerate(dtnc_step):
        #son racine + majeur ou mineur
        dtnc_chord_list.append(key_list[pos]+dtnc_chord_list_num[num][1:])
        pos += step
        #Positions express sous forme de nombre de 12 ou moins
        if pos >= len(key_list):
            pos -= len(key_list)
    #Stocke une liste de codes diatoniques pour 12 touches différentes
    dtnc_chord_list_arr.append(dtnc_chord_list)


for fname in flist:
    with open(fname,encoding='cp932',mode='r+') as f:
        f.readline().rstrip('\n')
        chord_list = f.readline().rstrip('\n')
        f.readline().rstrip('\n')
        lyric_list = f.readline().rstrip('\n')
        #♭#Convertir en
        chord_list = re.sub('({})'.format('|'.join(map(re.escape, r_dict.keys()))), lambda m: r_dict[m.group()], chord_list)
        chord_list = chord_list.split(',')
        chord_list_origin = chord_list.copy()
        # N.C.Se débarrasser de
        chord_list = [chord for chord in chord_list if "N" not in chord]
        #Triade uniquement
        def get_triad(chord):
            split_chord = list(chord)
            triad = split_chord[0]
            if len(split_chord )>=2 and split_chord[1]=='#':
                triad += split_chord[1]
                if len(split_chord )>=3 and split_chord[2]=='m':
                    triad += split_chord[2]
                    if len(split_chord )>=5 and split_chord[4]=='-':
                        triad += split_chord[3]
                        triad += split_chord[4]
                        triad += split_chord[5]

            elif len(split_chord )>=2 and split_chord[1]=='m':
                triad += split_chord[1]
                if len(split_chord )>=4 and split_chord[3]=='-':
                        triad += split_chord[2]
                        triad += split_chord[3]
                        triad += split_chord[4]
            else:
                pass

            return triad
        #Changer en triade
        chord_list_triad = [get_triad(chord) for chord in chord_list]
        length = len(chord_list)
        #Nombre unique de code
        chord_unique = collections.Counter(chord_list_triad)
        #print(chord_unique)

        #################
        ###Déterminez la clé###
        #################

        match_cnt_arr = []
        #Calculez le nombre de correspondances avec 12 types de clés
        for dtnc_chord_list in dtnc_chord_list_arr:
            match_cnt = 0
            #Accordez chacun des 7 codes diatoniques_Comparez avec la valeur de clé unique,
            #S'il y a une correspondance, faites correspondre_Ajouter à cnt
            for dtnc_chord in dtnc_chord_list:
                if dtnc_chord in chord_unique.keys():
                    match_cnt += chord_unique[dtnc_chord]
            match_cnt_arr.append(match_cnt)
        #Nombre maximum de correspondances parmi 12 types
        max_cnt = max(match_cnt_arr)
        #Nombre de codes correspondants/Nombre total de codes(%)
        match_prb = int((max_cnt/length)*100)

        #Détermination clé
        key_pos = match_cnt_arr.index(max_cnt)
        key = key_list[key_pos]
        dtnc_chord_list = dtnc_chord_list_arr[key_pos]
        file_name = os.path.basename(fname).replace('.csv','')
        print('Titre de la chanson:{0} , key:{1} , prob:{2}'.format(file_name,key,match_prb))
        print(dtnc_chord_list)
        key_list_chromatic = key_list[key_pos:] + key_list[:key_pos]
        # key_list_chromatic.extend(key_list[key_pos:])
        # key_list_chromatic.extend(key_list[:key_pos])
        print(key_list_chromatic)

        #Convertir le code en nombres romains et écrire dans un fichier
        #Fonction à convertir
        def convert_num_chord(chord_list):
            s_list = []
            n_list = []
            for idx, root in enumerate(key_list_chromatic):
                #Séparez les racines selon la présence ou l'absence d'affûtage pour remplacer celles à affûter en premier.
                if '#' in root:
                    s_list.append([idx,root])
                else:
                    n_list.append([idx,root])
            chord_list = ['*' if "N" in chord else chord for chord in chord_list]
            for idx,root in s_list:
                chord_list = [chord.replace(root,rome_list[idx]) if root in chord else chord for chord in chord_list]
            for idx,root in n_list:
                chord_list = [chord.replace(root,rome_list[idx]) if root in chord else chord for chord in chord_list]
            chord_list = ['N.C.' if "*" in chord else chord for chord in chord_list]
            
            return chord_list

        chord_list_converted = convert_num_chord(chord_list_origin)
        print(chord_list_origin)
        print(chord_list_converted)
    with open(fname, "w", encoding="cp932") as f:
        writer = csv.writer(f)
        writer.writerow('key:{0},prob:{1}'.format(key,match_prb).split(','))
        writer.writerow(chord_list_origin)
        writer.writerow(chord_list_converted)
        writer.writerow(lyric_list.split(','))

Comment était-ce. Il est long et difficile de comprendre ce que vous faites, mais vous pouvez reconnaître que vous l'avez standardisé. Ce n'est pas un gros problème, donc c'est fait en un instant. Lorsque j'exécute ce code, j'obtiens les résultats suivants: スクリーンショット 2020-11-01 17.43.45.png

Vous pouvez voir que key = D # sur la première ligne, la probabilité d'appartenance est de 67% et la progression du code des nombres romains normalisés est ajoutée sur la troisième ligne. (Puisque le code de l'aiko est assez compliqué et que de nombreux codes non diatoniques apparaissent, la probabilité d'appartenir à diatonique est de 67%. Même avec cela, la clé peut être estimée correctement, donc l'exactitude de ce processus est correcte. Je pense que vous pouvez le prouver.)

Maintenant, dans le dernier chapitre, vectorisons et dessinons ce code standardisé.

Chapitre 3: Vectoriser le code avec word2vec et le montrer avec t-SNE

Maintenant, mettons les données d'environ 200 chansons d'aiko dans word2vec et vectorisons-les. Le nombre de dimensions est défini sur 10 car il existe au maximum 100 types de code de sortie. De plus, pour window, le rôle du code peut être suffisamment jugé avec 4 codes avant et après, donc je l'ai spécifié comme 4. De plus, sg = 0 a été défini afin d'adopter CBOW (Continuous Bag-of-Words) qui estime le mot central à partir de la périphérie. Cela transforme chaque code en un vecteur à 10 dimensions. Afin de voir ce résultat sous forme de figure, il a été compressé en deux dimensions à l'aide de t-SNE et illustré sur la figure. Le paramètre de perplexité ici est un endroit difficile, mais je l'ai mis à 3 car le plus petit est plus facile à séparer pour chaque cluster.

S'il s'agit de compresser les dimensions, pourquoi ne pas rendre la dimension de représentation du code bidimensionnelle? Je suis sûr qu'il y a des gens qui pensent cela, mais il n'y a presque aucune différence dans le code, alors je l'ai fait en 10 dimensions pour le mettre sur l'expressivité.

Regardons maintenant le code.

analyze.py


from gensim.models import Word2Vec
import glob
import itertools
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

artist = "aiko"
#Répertoire des données
fdir = "chord_data/" + artist + "/"
#Progression du code de chargement
flist = glob.glob(fdir+"*")
music_list = []
for fname in flist:
    with open(fname,encoding='cp932',mode='r+') as f:
        f.readline().rstrip('\n')
        f.readline().rstrip('\n')
        chord_list = f.readline().rstrip('\n').split(',')
        f.readline().rstrip('\n')
        lyric_list = f.readline()
        chord_list = [chord for chord in chord_list if "N" not in chord]
        music_list.append(chord_list)

#Vectoriser le code
model = Word2Vec(music_list,sg=0,window=4,min_count=0,iter=100,size=10)
chord_unique = list(set(itertools.chain.from_iterable(music_list)))
data = [model[chord] for chord in chord_unique]
print(model.most_similar('Ⅱ'))


#Compresser en 2 dimensions et dessiner
tsne = TSNE(n_components=2, random_state = 0, perplexity = 3, n_iter = 1000)
data_tsne = tsne.fit_transform(data)

fig=plt.figure(figsize=(50,25),facecolor='w')

plt.rcParams["font.size"] = 10

for i,chord in enumerate(data_tsne):
    #Tracé de points
    plt.plot(data_tsne[i][0], data_tsne[i][1], ms=5.0, zorder=2, marker="x",color="red")
    plt.annotate(chord_unique[i],(data_tsne[i][0], data_tsne[i][1]), size=10)
    i += 1

#plt.show()
plt.savefig("chord_data/" + artist + ".jpg ")
 

Le résultat obtenu avec ce code est le suivant. aiko.jpg

aiko a de nombreux types de code et il est difficile de trouver un cluster, Par exemple, dans le cercle en bas à gauche du centre, les triades I, IV et V sont alignées à proximité. Ceci est également correct dans la théorie musicale, et cette liste de combinaisons d'accords est l'accord le plus fréquemment apparu, et en raison de sa pertinence, les vecteurs sont probablement plus proches. Dans le même cercle, il y a un code appelé II, mais bien que ce soit un code non diatonique, je pense qu'il était probable qu'il existait à proximité en tant que doppel dominant avant le code V. Il y a aussi # V et # VI, qui sont souvent utilisés dans la fameuse progression de # V → # VI → I, et on pense que ces deux codes sont proches l'un de l'autre. De plus, il y a un ensemble de codes avec 7ème dans le cercle supérieur, donc je pense que c'était un résultat intéressant à voir.

Comme c'est un gros problème, je publierai également les résultats d'autres artistes. Tout d'abord, ce qui suit est Official Beard Man dism. Official髭男dism.jpg

Et l'autre est un artiste appelé andymori. Cela peut être plus facile à lire car il utilise moins de code. andymori.jpg

Conclusion et perspectives d'avenir

En conclusion, j'ai trouvé que mettre le code dans word2vec conduit à des données significatives.

Comme perspective d'avenir, rappelons à l'avance la fameuse progression d'accords, et pour cela, divisons la chanson par environ 4 progressions d'accords. Il peut être intéressant de créer une matrice clairsemée en tant que progression d'accords, de créer un sujet dans LDA et de déterminer la similitude des chansons au sein d'un artiste. De plus, ce serait encore mieux si nous pouvions faire des recommandations aux artistes. Aussi, pour ceux qui ne s'intéressent qu'à la musique, j'aimerais avoir un outil qui permette de voir facilement où les changements sont apportés par le travail du chapitre 2.

Si vous écrivez un article qui combine musique et programmation, faites-vous des amis. ..

Merci pour la lecture.

Recommended Posts