Wagner2Vek: Die Meistersinger von Nürnberg

Word2Vecによる楽劇「ニュルンベルクのマイスタージンガー」(リヒャルト・ワーグナー作曲のオペラ)のドイツ語歌詞の解析と関係性可視化の試み

Executive Summary

  • http://www.murashev.com/をスクレイピングして歌詞だけを抽出(元々はwebサイトの記述やト書き、役名が含まれている)、DataFrameに整形。
    • requetsでhtmlソースを取得し、正規表現(とBeautifulSoup)でクレンジングした。
  • spaCyを用いて単語(トークン)をレンマ化し、名詞、動詞、形容詞、副詞、固有名詞を残した。
    • NLTKはドイツ語に未対応の模様。
  • gensimのWord2Vecを用いて、単語相互のsimilarityを算出した。
    • gensimの再現性を確保するために、ハッシュ値を返す簡易な関数を自作した。
  • NetworkXとBokehを用いて、インタラクティヴなネットワーク図を作成した。

    • 見やすさを高めるため、出現頻度が20以上のノード、similarityが0.2超のedgeに限定した。
    • @komo_frによるPEP解析を参考にした。
  • https://github.com/ytknzw/wagner/blob/master/Wagner2Vek_Meistersinger.ipynb

ライブラリの読込

In [1]:
%matplotlib inline
# スクレイピング・データ整形
import requests
from bs4 import BeautifulSoup
import re
import pandas as pd
# NLP
import spacy
import hashlib
import gensim
import codecs
from gensim.models import word2vec
from gensim.models.phrases import Phrases, Phraser
# ネットワーク図作成
import networkx as nx
import matplotlib.pyplot as plt
from bokeh.io import show, output_notebook
from bokeh.plotting import figure
from bokeh.models import Plot, Range1d, MultiLine, Circle,\
                         HoverTool, BoxZoomTool, ResetTool, TapTool, BoxSelectTool, WheelZoomTool, PanTool, SaveTool,\
                         ColumnDataSource, LabelSet
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4

スクレイピング・データ整形用の関数を定義

In [2]:
def remove_headfoot(responce):
    """
    Remove headers and footers from a responce
    
    Arguments
    responce: responce of requests.get()
    
    Return values
    text: text without headers and footers
    """
    responce.encoding = responce.apparent_encoding
    text = re.sub('(<br />|\s{2,})(?=[0-9\u00c4\u00d6\u00dc\u00dfA-Z][\s.0-9\u00c4\u00d6\u00dc\u00dfA-Z]{2,}<br />)', '<br /><br />',
              re.sub('<tr valign="top"><td>libretto by.+', '',
                     re.findall('<span class="act".+', re.sub('[\n\r\t]', '', responce.text))[0]))
    return text
In [3]:
def text2df(text):
    """
    Shape and cleanse text into a dataframe
    
    Arguments
    text: output of extract_text()
    
    Return Values
    df: a dataframe into which the text gets shaped
    """
    df = pd.DataFrame()
    texts = text.split('<b>')
    aufzug = BeautifulSoup(texts.pop(0)).text
    print(aufzug)
    for t in texts:
        szene = t.split('</b>')[0]
        print(szene)
        script = re.sub('(<br />)?<i>([\w\s\(\)\[\];,:.-]|<br />|</?t(d|r)[\w\s"=/]*>)+</i>', '', t.split('</b>')[1]).split('<br /><br />')
        for s in script:
            if s not in (None, ''):
                s_list = re.sub('^<br />', '', s).split('<br />')
                if len(s_list) > 1:
                    name = re.sub('<[\w\s"=/]+>|\s{2,}', '', s_list.pop(0))
                    s_text = re.sub('<[\w\s"=/]+>|\s{2,}', '', ' '.join(s_list))
                    df = pd.concat([df, pd.DataFrame({'aufzug': aufzug, 'szene': szene, 'name': name, 'text': s_text}, index=['szene'])])
    return df.reset_index(drop=True)

データ取得・整形

In [4]:
dat = pd.DataFrame()
for i in range(1, 4):
    res = requests.get(f'http://www.murashev.com/opera/Die_Meistersinger_von_N%C3%BCrnberg_libretto_German_Act_{i}')
    text = remove_headfoot(res)
    dat = pd.concat([dat, text2df(text)])
ERSTER AUFZUG
ERSTE SZENE
ZWEITE SZENE
DRITTE SZENE
ZWEITER AUFZUG
ERSTE SZENE
ZWEITE SZENE
DRITTE SZENE
VIERTE SZENE
FÜNFTE SZENE
SECHSTE SZENE
SIEBENTE SZENE
DRITTER AUFZUG
VORSPIEL
ERSTE SZENE
ZWEITE SZENE
DRITTE SZENE
VIERTE SZENE
FÜNFTE SZENE

内容を確認。

In [5]:
dat
Out[5]:
aufzug szene name text
0 ERSTER AUFZUG ERSTE SZENE CHORAL DER GEMEINDE Da zu dir der Heiland kam, willig seine Taufe ...
1 ERSTER AUFZUG ERSTE SZENE WALTHER Verweilt! Ein Wort! Ein einzig Wort!
2 ERSTER AUFZUG ERSTE SZENE EVA Mein Brusttuch! schau! Wohl liegt's im Ort -
3 ERSTER AUFZUG ERSTE SZENE MAGDALENE Vergesslich Kind! Nun heisst es: such'!
4 ERSTER AUFZUG ERSTE SZENE WALTHER Fräulein, verzeiht der Sitte Bruch. Eines zu w...
5 ERSTER AUFZUG ERSTE SZENE MAGDALENE Hier ist das Tuch.
6 ERSTER AUFZUG ERSTE SZENE EVA O weh! Die Spange.
7 ERSTER AUFZUG ERSTE SZENE MAGDALENE Fiel sie wohl ab?
8 ERSTER AUFZUG ERSTE SZENE WALTHER Ob Licht und Lust, oder Nacht und Grab? Ob ich...
9 ERSTER AUFZUG ERSTE SZENE MAGDALENE Da ist auch die Spange. - Komm, Kind! Nun hast...
10 ERSTER AUFZUG ERSTE SZENE WALTHER Dies eine Wort, ihr sagt mir's nicht? Die Silb...
11 ERSTER AUFZUG ERSTE SZENE MAGDALENE Sieh da! Herr Ritter? Wie sind wir hochgeehrt:...
12 ERSTER AUFZUG ERSTE SZENE WALTHER O, betrat ich doch nie sein Haus!
13 ERSTER AUFZUG ERSTE SZENE MAGDALENE Ei! Junker, was sagt ihr da aus? In Nürnberg e...
14 ERSTER AUFZUG ERSTE SZENE EVA Gut Lenchen, ach! das meint er ja nicht; doch ...
15 ERSTER AUFZUG ERSTE SZENE MAGDALENE Hilf Gott! Sprich nicht so laut! jetzt lass un...
16 ERSTER AUFZUG ERSTE SZENE WALTHER Nicht eh'r, bis ich alles weiss!
17 ERSTER AUFZUG ERSTE SZENE EVA 's ist leer, die Leut' sind fort.
18 ERSTER AUFZUG ERSTE SZENE MAGDALENE Drum eben wird mir heiss! Herr Ritter, an andr...
19 ERSTER AUFZUG ERSTE SZENE WALTHER Nein! Erst dies Wort!
20 ERSTER AUFZUG ERSTE SZENE EVA Dies Wort?
21 ERSTER AUFZUG ERSTE SZENE MAGDALENE David! Ei! David hier?
22 ERSTER AUFZUG ERSTE SZENE EVA Was sag' ich? Sag' du's mir!
23 ERSTER AUFZUG ERSTE SZENE MAGDALENE Herr Ritter, was ihr die Jungfer fragt, das is...
24 ERSTER AUFZUG ERSTE SZENE EVA Doch hat noch keiner den Bräut'gam erschaut!
25 ERSTER AUFZUG ERSTE SZENE MAGDALENE Den Bräut'gam wohl noch niemand kennt, bis mor...
26 ERSTER AUFZUG ERSTE SZENE EVA Und selbst die Braut ihm reicht das Reis.
27 ERSTER AUFZUG ERSTE SZENE WALTHER Dem Meistersinger?
28 ERSTER AUFZUG ERSTE SZENE EVA Seid ihr das nicht?
29 ERSTER AUFZUG ERSTE SZENE WALTHER Ein Werbgesang?
... ... ... ... ...
191 DRITTER AUFZUG FÜNFTE SZENE ORTEL UND FOLTZ Welch' eigner Fall!
192 DRITTER AUFZUG FÜNFTE SZENE SACHS Das Lied, fürwahr, ist nicht von mir: Herr Bec...
193 DRITTER AUFZUG FÜNFTE SZENE DIE MEISTERSINGER Wie? Schön das Lied? Der Unsinns-Wust?
194 DRITTER AUFZUG FÜNFTE SZENE VOLK Hört! Sachs macht Spass! Er sagt es nur zur Lust.
195 DRITTER AUFZUG FÜNFTE SZENE SACHS Ich sag' euch Herrn, das Lied ist schön; nur i...
196 DRITTER AUFZUG FÜNFTE SZENE DIE MEISTERSINGER Ei Sachs, ihr seid gar fein! Doch mag es heut'...
197 DRITTER AUFZUG FÜNFTE SZENE SACHS Der Regel Güte daraus man erwägt, dass sie auc...
198 DRITTER AUFZUG FÜNFTE SZENE VOLK Ein guter Zeuge, stolz und kühn; mich dünkt, d...
199 DRITTER AUFZUG FÜNFTE SZENE SACHS Meister und Volk sind gewillt zu vernehmen, wa...
200 DRITTER AUFZUG FÜNFTE SZENE LEHRBUBEN Alles gespannt! 's gibt kein Gesumm': da rufen...
201 DRITTER AUFZUG FÜNFTE SZENE WALTHER "Morgenlich leuchtend im rosigen Schein, von B...
202 DRITTER AUFZUG FÜNFTE SZENE MEISTERSINGER Ja wohl, ich merk', 's ist ein ander Ding, ob ...
203 DRITTER AUFZUG FÜNFTE SZENE VOLK Das ist was andres, wer hätt's gedacht; was do...
204 DRITTER AUFZUG FÜNFTE SZENE SACHS Zeuge am Ort, fahret fort!
205 DRITTER AUFZUG FÜNFTE SZENE WALTHER "Abendlich dämmernd umschloss mich die Nacht; ...
206 DRITTER AUFZUG FÜNFTE SZENE MEISTERSINGER 's ist kühn und seltsam, das ist wahr; doch wo...
207 DRITTER AUFZUG FÜNFTE SZENE VOLK So hold und traut so fern es schwebt; doch ist...
208 DRITTER AUFZUG FÜNFTE SZENE SACHS Zeuge, wohl erkiest; Fahret fort, und schliesst!
209 DRITTER AUFZUG FÜNFTE SZENE WALTHER "Huldreichster Tag, dem ich aus Dichters Traum...
210 DRITTER AUFZUG FÜNFTE SZENE VOLK Gewiegt wie in den schönsten Traum, hör' ich e...
211 DRITTER AUFZUG FÜNFTE SZENE DIE MEISTER Ja, holder Sänger, nimm das Reis; dein Sang er...
212 DRITTER AUFZUG FÜNFTE SZENE POGNER O Sachs! Dir dank' ich Glück und Ehr': vorüber...
213 DRITTER AUFZUG FÜNFTE SZENE EVA Keiner wie du so hold zu werben weiss!
214 DRITTER AUFZUG FÜNFTE SZENE SACHS Den Zeugen, denk' es, wählt' ich gut: tragt ih...
215 DRITTER AUFZUG FÜNFTE SZENE VOLK Hans Sachs! Nein! Das war schön erdacht! Das h...
216 DRITTER AUFZUG FÜNFTE SZENE MEISTERSINGER Auf, Meister Pogner! Euch zum Ruhm, meldet dem...
217 DRITTER AUFZUG FÜNFTE SZENE POGNER Geschmückt mit König Davids Bild, nehm' ich eu...
218 DRITTER AUFZUG FÜNFTE SZENE WALTHER Nicht Meister! Nein! Will ohne Meister selig s...
219 DRITTER AUFZUG FÜNFTE SZENE SACHS Verachtet mir die Meister nicht, und ehrt mir ...
220 DRITTER AUFZUG FÜNFTE SZENE VOLK Ehrt eure deutschen Meister, dann bannt ihr gu...

801 rows × 4 columns

In [6]:
for i, t in enumerate(dat.text):
    res = re.findall('[\s.0-9\u00c4\u00d6\u00dc\u00dfA-Z]{3,}', t)
    if len(res) > 0:
        print(i)
        print(res)
0
['. E']
4
['. E', '...']
8
['...']
9
['... O ']
23
['. F']
50
['. M']
53
['. W']
55
['. D']
57
['. F']
86
['O L']
88
['O M']
90
['. I', '. S', '. D']
92
['. U', '. W']
96
['. J', '. M', '. N', '. V', '. S', '. W']
107
['. E', '. D', '. S']
114
['. W']
118
['. V']
120
['. W']
129
['. D']
134
['. S']
162
['. B']
165
['. F']
167
['. D', '. Z', '. N', '. A', '. D', '. D']
179
['. E']
180
['. E', '. W']
183
['. D']
185
['. D', '. D']
191
['. S']
199
['. M']
203
['. T']
205
['. A']
225
['. W']
227
['. D']
228
['. H', '. S']
229
['. E', '. D', '. D']
235
['. E', '. D', '. D']
243
['. D', '. Z']
253
['. W']
255
['. D']
268
['. W']
269
['. E']
281
['. E']
288
['. D']
300
['.O K']
303
['. D']
308
['. N']
317
['. N']
321
['. D']
328
['. W', '. E', '. D']
343
['. I']
355
['. G']
366
['. H']
369
['. N']
382
['. N']
395
['. J']
405
['. H']
411
['. D', '. F', '. S']
412
['. U']
418
['. L']
422
['O H']
442
['. W']
446
['. L']
451
['...']
454
['...']
461
[' O E']
478
[' O ']
480
['O E']
482
[' O ']
486
[' O ']
490
['. A']
491
['. F']
495
['. S']
496
['. V', '. V']
500
['. O E']
501
['. N']
514
['. D', '. D', '. D']
542
['. W']
543
['. S']
547
['. D', '. N']
550
['. D', '. G', '. N']
551
['. N', '. W', '. E']
552
['. M']
553
['. D', '. N']
570
['. H', '. S', '. M']
571
['. W']
580
['. M', '. S', '. W', '. D', '. W', '. A']
581
['. W']
592
['. S']
593
['. D']
601
['. J', '. K']
603
['. G', '. D']
610
['. G', '. W']
614
['. W', '. E']
618
['. K']
624
['. S']
632
['. G']
638
['. N']
642
['. J', '. A']
643
['. N', '. L']
644
['. W']
646
['. U']
648
['. E', '. D', '. D']
653
['. D']
655
['. D', '. D', '. J', '. G', '. D']
664
['. S']
687
['. W', '. W', '. E']
693
['. D', '. D', '. H', '. A']
694
['. D', '. S']
701
['. W']
713
['. K', '. N', '. K', '. D', '. S']
720
['. D', '. E', '. W']
721
['O S', '. D', '. D']
722
['. N', '. V', '. W', '. D', '. K', '. F', '. D', '. N', '. D']
724
['. O', '. D']
726
['. O']
727
['. W']
728
['. D']
729
['. D', '. D']
730
['. H', '. D']
731
['. B']
743
['. S', '. S', '. U', '. W', '. E', '. D', '. I']
744
['O S']
756
['. A']
762
['... M']
764
['. B', '. M']
766
['. M']
772
['. W']
775
['. I', '. I']
779
['. H']
781
['. E']
790
['. R']
792
['O S']
799
['. N', '. D', '. D']

単語数

In [7]:
len(' '.join(dat.text).split())
Out[7]:
16141

spaCyによるドイツ語テキスト処理

In [8]:
de = spacy.load('de')
In [9]:
dok = de(' '.join(dat.text))
#dok
In [10]:
# 最初の20個のトークンについて処理結果を確認。
for t in dok[0:20]:
    print(t.text, t.norm_, t.lemma_, t.pos_, t.is_stop, t.is_alpha, t.is_punct)
Da da da ADV False True False
zu zu zu ADP True True False
dir dir sich PRON True True False
der der der DET True True False
Heiland heiland Heiland PROPN False True False
kam kam kommen VERB True True False
, , , PUNCT False False True
willig willig willig ADJ False True False
seine seine mein DET True True False
Taufe taufe Taufe NOUN False True False
nahm nahm nehmen VERB True True False
, , , PUNCT False False True
weihte weihte weihen VERB False True False
sich sich sich PRON True True False
dem dem der DET True True False
Opfertod opfertod Opfertod NOUN False True False
, , , PUNCT False False True
gab gab geben VERB True True False
er er ich PRON True True False
uns uns sich PRON True True False
In [11]:
t_lemma = ''
for t in dok:
    if ~t.is_stop and t.is_alpha and t.pos_ in ['NOUN', 'VERB', 'ADJ', 'PROPN', 'ADV']:
        # ストップ語以外、アルファベットからなる語、品詞が名詞、動詞、形容詞、固有名詞、副詞。
        t_lemma = t_lemma + '\n' + t.lemma_
#t_lemma
In [12]:
# with codecs.open("Meistersinger.txt", "w", "utf-8") as f:
#     f.write(t_lemma)

# with codecs.open("Meistersinger.txt", "r", "utf-8") as f:
#     corpus = f.read().splitlines()
#     print(corpus)

gensimのWord2Vecを用いて、単語相互のsimilarityを算出

In [13]:
corpus = [sentence.split() for sentence in t_lemma.splitlines()]
In [14]:
# https://stackoverflow.com/questions/34831551/ensure-the-gensim-generate-the-same-word2vec-model-for-different-runs-on-the-sam/34849797
# gensimの再現性を確保するための自前ハッシュ関数
def dummyhash(string):
    hash_value = int(hashlib.sha256(string.encode()).hexdigest(), 16)
    return hash_value
In [15]:
# モデルを作成
model = word2vec.Word2Vec(corpus, size=100, min_count=3, window=5, hs=1, negative=0,
                          # gensimの再現性を確保するための設定
                          workers=1, hashfxn=dummyhash)
In [16]:
model.save('Meistersinger.model')
# 上で保存したモデルを読み込む: model = Word2Vec.load('Meistersinger.model')
/Users/yuta/anaconda3/lib/python3.7/site-packages/smart_open/smart_open_lib.py:398: UserWarning: This function is deprecated, use smart_open.open instead. See the migration notes for details: https://github.com/RaRe-Technologies/smart_open/blob/master/README.rst#migrating-to-the-new-open-function
  'See the migration notes for details: %s' % _MIGRATION_NOTES_URL

モデルの内容を確認する。

In [17]:
model.wv.vocab.keys()
Out[17]:
dict_keys(['da', 'kommen', 'Taufe', 'nehmen', 'weihen', 'geben', 'Gebot', 'wert', 'dort', 'Jordan', 'Wort', 'einzig', 'Wohl', 'liegen', 'Ort', 'Kind', 'Nun', 'heisst', 'Fräulein', 'Bruch', 'wissen', 'fragen', 'müsst', 'brechen', 'wagen', 'Leben', 'vertrauen', 'sagen', 'Hier', 'O', 'wohl', 'Licht', 'Lust', 'Nacht', 'verlangen', 'vernehmen', 'auch', 'Komm', 'selbst', 'buchen', 'Silbe', 'urteilen', 'sprechen', 'schon', 'Braut', 'Sieh', 'Herr', 'Ritter', 'wie', 'Evchens', 'gar', 'dürfen', 'meist', 'Pogner', 'doch', 'nie', 'hausen', 'Ei', 'Junker', 'Nürnberg', 'eben', 'nur', 'sein', 'bieten', 'Dank', 'Gut', 'meinen', 'ja', 'schnellen', 'kaum', 'Traum', 'Hilf', 'Gott', 'so', 'laut', 'jetzt', 'lass', 'Leut', 'hier', 'weiss', 'Drum', 'heiss', 'David', 'Jungfer', 'Evchen', 'noch', 'kennen', 'morgen', 'Gericht', 'Meistersinger', 'erteilen', 'preisen', 'reichen', 'reisen', 'gewinnen', 'mein', 'dann', 'wählen', 'Sinn', 'Lene', 'helfen', 'gestern', 'schaffen', 'Qual', 'sehen', 'ganz', 'nah', 'tollen', 'Bild', 'lang', 'Schwert', 'Hand', 'rufen', 'lieb', 'Herz', 'allein', 'treiben', 'Posse', 'Gar', 'dingen', 'richt', 'Gäb', 'Singen', 'Nur', 'Freiung', 'heut', 'Tabulatur', 'recht', 'Jetzt', 'komm', 'müssen', 'lasst', 'bald', 'wollen', 'Zeit', 'glücken', 'hinnen', 'soll', 'beginnen', 'Lasst', 'lehren', 'begehren', 'Hör', 'Gesell', 'bewahr', 'Stell', 'wieder', 'gewiss', 'können', 'neu', 'sinnen', 'begreif', 'muss', 'gelingen', 'gelten', 'ersingen', 'Blut', 'Dichter', 'Mut', 'gehen', 'gut', 'Gleich', 'Oho', 'stehen', 'werken', 'richten', 'Gemerk', 'nun', 'dünken', 'machen', 'schustern', 'Leiste', 'sitzen', 'federn', 'Draht', 'Vers', 'schreiben', 'ledern', 'dächt', 'Fanget', 'Merker', 'sollen', 'singen', 'Noch', 'wo', 'Richter', 'dicht', 'Wär', 'Singer', 'Schüler', 'klingen', 'Ohr', 'grosse', 'Beschwerde', 'tun', 'tagen', 'Nürenberg', 'Kunst', 'Hans', 'Sachs', 'Schon', 'voll', 'Jahr', 'wachs', 'Poeterei', 'lern', 'glatt', 'schlagen', 'weichen', 'erst', 'zahlen', 'langen', 'kurz', 'hart', 'hellen', 'blind', 'Blume', 'lernen', 'Sorg', 'Acht', 'weit', 'bringen', 'paaren', 'Schuh', 'dahin', 'Ruh', 'Bar', 'Gebänd', 'gleichen', 'regeln', 'fänd', 'Stolle', 'Abgesang', 'Reim', 'stellen', 'merken', 'immer', 'denn', 'glauben', 'Nam', 'stark', 'leise', 'Ton', 'blau', 'süsse', 'frischen', 'grün', 'treu', 'himmeln', 'Name', 'steigen', 'Stimm', 'fällen', 'fangen', 'hoch', 'tief', 'End', 'stimmen', 'fest', 'Meister', 'haben', 'versungen', 'oft', 'Hilfe', 'dran', 'eh', 'richtig', 'He', 'falsch', 'nennen', 'damit', 'Weise', 'erkennen', 'bleiben', 'find', 'mach', 'fehl', 'Stuhl', 'Singschul', 'wisst', 'Ehre', 'sichern', 'freit', 'lachen', 'andrer', 'Sprung', 'denken', 'fein', 'Tafel', 'bang', 'Werber', 'versang', 'Fehler', 'kreiden', 'verlieren', 'vertuen', 'wachen', 'Meistersingen', 'mögen', 'Blumenkränzlein', 'Seide', 'Herrn', 'beschieden', 'Wettgesang', 'streichen', 'gelegen', 'Tochter', 'bitt', 'werben', 'Beckmesser', 'gern', 'lässt', 'wehren', 'suchen', 'schlecht', 'frei', 'Lieb', 'möcht', 'Zunft', 'Vogelgesang', 'Freund', 'Nachtigall', 'Hört', 'Fall', 'wenden', 'still', 'hören', 'Lied', 'schwören', 'Mensch', 'Glaubt', 'freuen', 'alt', 'gefallen', 'blicken', 'nehm', 'Sixtus', 'Güte', 'hoffen', 'Kopf', 'schlag', 'nenn', 'Veit', 'finden', 'Immer', 'Bleibt', 'fehlen', 'Vogel', 'Schweigt', 'Schön', 'Fell', 'Verzeiht', 'blüh', 'schreiten', 'Fest', 'Amt', 'verstehen', 'Johannistag', 'Au', 'spielen', 'tanzen', 'froh', 'Brust', 'bergen', 'vergessen', 'Sorge', 'weisen', 'Volk', 'lassen', 'lauschen', 'rühmen', 'Mann', 'deutsch', 'viel', 'bürgern', 'Hof', 'Reich', 'schön', 'ward', 'Welt', 'zeigen', 'gewillt', 'drum', 'erringen', 'Sankt', 'Eva', 'breit', 'ledig', 'Weib', 'raten', 'klug', 'Stimmt', 'andren', 'Vielleicht', 'gleich', 'Frau', 'ehren', 'Wahl', 'eigen', 'Spruch', 'Ade', 'Gewiss', 'Regel', 'Vernehmt', 'kenn', 'einmal', 'Kraft', 'spuren', 'Hei', 'Johannisfest', 'geschehen', 'bestellen', 'halt', 'maulen', 'Schmach', 'laufen', 'Gunst', 'dichten', 'frag', 'wecken', 'Junggesell', 'Witwer', 'Wachs', 'jung', 'Stolzing', 'Dacht', 'Geht', 'spät', 'gross', 'willkommen', 'Frage', 'gebären', 'edel', 'Walther', 'verliess', 'ziehen', 'einen', 'Lenz', 'lieblich', 'erwachen', 'lesen', 'Vogelweid', 'Schul', 'mocht', 'Wald', 'Pracht', 'darnach', 'fasst', 'loben', 'Gesang', 'bereiten', 'geraten', 'Meisterlied', 'Weis', 'Sang', 'werd', 'heilig', 'lieben', 'schliesst', 'stillen', 'halten', 'Melodei', 'folgen', 'bewahren', 'Meisterpreis', 'setzen', 'brauchen', 'fliehen', 'schwellen', 'hold', 'wachsen', 'musst', 'Raum', 'warm', 'fertigen', 'Singt', 'schauen', 'zwar', 'Arbeit', 'Meinung', 'Sagt', 'toll', 'stolz', 'davon', 'Auch', 'erklären', 'verwirren', 'Aha', 'leicht', 'Gasse', 'Darum', 'Schluss', 'ganze', 'Füssen', 'Zwist', 'streiten', 'sorgen', 'Poet', 'übel', 'Schuhwerk', 'überall', 'klappen', 'daheim', 'dazu', 'Sprüchlein', 'Sohl', 'Stadtschreiber', 'arm', 'seh', 'quälen', 'Luft', 'selig', 'hin', 'Flucht', 'winken', 'schmerzen', 'Not', 'kühn', 'Stadt', 'hehr', 'Ha', 'schweigen', 'flecken', 'Bänder', 'Bst', 'dumm', 'aber', 'heilen', 'gefreit', 'Alte', 'Schliess', 'Hab', 'Lass', 'scheinen', 'liess', 'mild', 'deuten', 'erscheinen', 'Gemahl', 'Vater', 'tritt', 'gasen', 'drin', 'Dann', 'Zeig', 'her', 'Dort', 'Bett', 'gescheit', 'warum', 'heute', 'behalten', 'fass', 'fassen', 'Füss', 'trauen', 'tragen', 'dachen', 'wüsst', 'mehr', 'merk', 'Pech', 'Fuss', 'Beckmessers', 'tüchtig', 'Dunst', 'Arme', 'genug', 'erdenken', 'fallen', 'Macht', 'Land', 'schlimm', 'Stand', 'Müh', 'dabei', 'Haufen', 'riechen', 'schaff', 'Fenster', 'teuer', 'sonst', 'schlafen', 'Preis', 'Freundin', 'erkiesen', 'böse', 'geistern', 'kreischen', 'Schad', 'Weg', 'hör', 'Nachts', 'Jerum', 'hallo', 'Tralalei', 'Ohe', 'Paradies', 'Kies', 'Engel', 'darin', 'Teufel', 'hallohe', 'Je', 'nachts', 'frisch', 'dritt', 'schwer', 'schelten', 'Verstand', 'Witz', 'Pognerin', 'Ehr', 'Kerl', 'Geselle', 'schwör', 'schändlich', 'je', 'brennen', 'Wahn', 'fahren', 'mal', 'irren', 'júng', 'Hoffnung', 'Seht', 'ohn', 'klaren', 'Takt', 'Gebt', 'Schlägerei', 'schneidern', 'Prügel', 'Schert', 'etwa', 'Zank', 'grad', 'Polterabend', 'Johannes', 'herrlich', 'gedenken', 'lohnen', 'wähnen', 'Tat', 'deut', 'Traumbild', 'Recht', 'Morgentraum', 'Morgen', 'leuchten', 'rosig', 'Duft', 'Wonne', 'ersinnen', 'Garten', 'laden', 'Frucht', 'wund', 'Auge', 'Stern', 'süss', 'kreisen', 'Feind', 'Werbung', 'Gedicht', 'wundern', 'Beschwer', 'drücken', 'streck', 'Sonne', 'rein', 'wahren', 'zugleich', 'Zeuge', 'Selig', 'Herzen', 'bezwingen', 'Hungersnot', 'Meck', 'Beck', 'hübsch', 'Silentium', 'Gesumm', 'zeugen'])
In [18]:
len(model.wv.vocab.keys())
Out[18]:
684
In [19]:
model.wv['Walther']
Out[19]:
array([ 0.00442194, -0.00201003, -0.00147538, -0.00196012, -0.00153767,
       -0.00442717, -0.00277035, -0.00364695,  0.00472021, -0.00494607,
       -0.00165588,  0.0042069 , -0.00376471,  0.00148012,  0.00342673,
        0.00273931, -0.00344558, -0.00369395, -0.00061749, -0.00397292,
        0.00487477,  0.00120018,  0.00395154, -0.000316  ,  0.0036352 ,
       -0.00085576, -0.00230499, -0.00476108,  0.00472589, -0.00061947,
       -0.00411484, -0.00127667, -0.00169091, -0.00458427, -0.00039572,
        0.00355952, -0.00242617, -0.00431572, -0.00114123, -0.00270527,
       -0.00230672, -0.00161435,  0.00334767,  0.0039234 , -0.00140781,
       -0.00049296,  0.00340537, -0.00314988,  0.00090166, -0.00215804,
        0.00201576, -0.00248295, -0.00205234,  0.00346181,  0.00011888,
        0.00160159,  0.0025362 ,  0.00226369,  0.00272002, -0.00129275,
       -0.00048919, -0.00140805,  0.00307381,  0.00029356, -0.00151785,
       -0.00094801,  0.00090223,  0.00232192,  0.00312414, -0.00408358,
       -0.00254486,  0.00376354,  0.00197258, -0.00198124,  0.00399658,
       -0.00419415, -0.00420664,  0.00215022, -0.00424763, -0.00495242,
        0.00053979, -0.00420276,  0.00456864, -0.00082709, -0.00065009,
       -0.00377939,  0.00125757, -0.00317055, -0.00031054, -0.00053405,
        0.00390343,  0.00230473, -0.00269764,  0.00410702,  0.00170862,
       -0.00109518, -0.00338518, -0.0044219 , -0.00484692,  0.00200559],
      dtype=float32)

単語間のsimilarity

In [20]:
model.wv.similarity('Walther', 'Beckmesser')
Out[20]:
0.03596978309773681
In [21]:
model.wv.similarity('Walther', 'Sachs')
Out[21]:
0.057208876041453
In [22]:
model.wv.similarity('Walther', 'Meistersinger')
Out[22]:
-0.07759305962630868
In [23]:
model.wv.similarity('David', 'Meistersinger')
Out[23]:
0.051510593796034704
In [24]:
model.wv.similarity('Gott', 'Lied')
Out[24]:
-0.06911376195266614

ある単語に近しい単語

In [25]:
similar_words = model.wv.most_similar(positive=["Walther"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
Jahr 0.32
schreiten 0.30
dazu 0.26
heisst 0.26
Dann 0.24
Hei 0.22
süsse 0.22
Witz 0.21
Junker 0.21
eh 0.21
In [26]:
similar_words = model.wv.most_similar(positive=["Stolzing"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
Herr 0.38
Vielleicht 0.32
Schuh 0.32
Takt 0.30
stillen 0.26
Ade 0.26
Stuhl 0.25
hausen 0.24
dumm 0.24
Wahn 0.23
In [27]:
similar_words = model.wv.most_similar(positive=["Junker"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
geben 0.32
erdenken 0.30
kühn 0.26
regeln 0.25
Freund 0.25
schreiten 0.25
Dank 0.24
seh 0.24
Herr 0.23
freuen 0.22
In [28]:
similar_words = model.wv.most_similar(positive=["Eva"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
dächt 0.32
Beschwerde 0.29
Füssen 0.28
glatt 0.28
gestern 0.28
Kopf 0.27
versang 0.26
oft 0.24
Wahl 0.24
hallo 0.23
In [29]:
similar_words = model.wv.most_similar(positive=["Beckmesser"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
Teufel 0.28
etwa 0.28
gelten 0.27
streiten 0.27
Sachs 0.24
stolz 0.21
übel 0.20
Richter 0.20
wisst 0.19
nennen 0.19
In [30]:
similar_words = model.wv.most_similar(positive=["Sachs"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
Teufel 0.33
kreisen 0.29
gestern 0.26
etwa 0.26
morgen 0.25
Beckmesser 0.24
hallo 0.23
böse 0.22
Bar 0.22
klug 0.22
In [31]:
similar_words = model.wv.most_similar(positive=["David"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
Schwert 0.39
schmerzen 0.36
genug 0.31
Kies 0.28
darin 0.26
fasst 0.25
still 0.25
Vers 0.24
Beck 0.23
fassen 0.23
In [32]:
similar_words = model.wv.most_similar(positive=["Lieb"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
langen 0.27
Schluss 0.27
genug 0.25
schneidern 0.24
fahren 0.24
Feind 0.23
Komm 0.23
fragen 0.22
Freundin 0.22
Zank 0.22
In [33]:
similar_words = model.wv.most_similar(positive=["Lied"], topn=10)
print(*[" ".join([v, str("{:.2f}".format(s))]) for v, s in similar_words], sep="\n")
richt 0.31
wüsst 0.26
Gesang 0.26
Jordan 0.23
fasst 0.23
davon 0.22
Wettgesang 0.22
Dort 0.21
Auge 0.20
Taufe 0.20

2単語間の差分

In [34]:
results = model.wv.most_similar(positive=['Walther'], negative=['Beckmesser'], topn=10)
for result in results:
    print(result)
('dazu', 0.34546202421188354)
('wähnen', 0.31116795539855957)
('Meister', 0.30161553621292114)
('Jahr', 0.2737889587879181)
('neu', 0.24289995431900024)
('Junker', 0.2399974763393402)
('schreiten', 0.23720328509807587)
('Meisterlied', 0.23618724942207336)
('freuen', 0.23415091633796692)
('dritt', 0.21885886788368225)
In [35]:
results = model.wv.most_similar(positive=['Sachs'], negative=['Beckmesser'], topn=10)
for result in results:
    print(result)
('leicht', 0.28681787848472595)
('kreisen', 0.2638789415359497)
('Beschwer', 0.2516334354877472)
('Garten', 0.2511705160140991)
('weit', 0.2496204823255539)
('gestern', 0.24838677048683167)
('Kind', 0.24051949381828308)
('Jerum', 0.2219531387090683)
('morgen', 0.2168348729610443)
('Alte', 0.2111724615097046)
In [36]:
results = model.wv.most_similar(positive=['Sachs'], negative=['Pogner'], topn=10)
for result in results:
    print(result)
('Teufel', 0.3174097537994385)
('hoch', 0.272392600774765)
('Bar', 0.25663185119628906)
('Wär', 0.23354312777519226)
('Schon', 0.23342682421207428)
('deut', 0.2247566133737564)
('liegen', 0.2218894064426422)
('Morgen', 0.2215660661458969)
('langen', 0.221276193857193)
('Ei', 0.2201652228832245)

NetworkXとBokehによる可視化

見やすいネットワーク図になるよう、出現頻度が20以上のノード、similarityが0.2超のedgeに限定した。

In [37]:
vokab = []
freq = []
for wort, vokab_obj in model.wv.vocab.items():
    if vokab_obj.count >= 20 or wort in ['Walther', 'Eva', 'Beckmesser', 'Sachs', 'Pogner', 'Meistersinger', 'David']:
        vokab += [wort]
        freq += [vokab_obj.count]

出現頻度の分布確認

In [38]:
pd.Series(freq).value_counts().sort_index()
Out[38]:
5      1
9      1
11     2
19     1
20     6
21     3
22     1
23     4
24     7
25     6
26     3
27     2
29     4
30     1
31     2
32     2
33     5
34     1
35     2
36     2
37     1
40     1
47     1
50     1
52     1
56     1
60     1
61     1
63     1
67     2
72     1
77     1
85     1
94     1
106    1
133    1
134    1
135    1
dtype: int64
In [39]:
vokab[0:20]
Out[39]:
['da',
 'kommen',
 'geben',
 'dort',
 'Wort',
 'Kind',
 'Nun',
 'sagen',
 'O',
 'wohl',
 'auch',
 'selbst',
 'schon',
 'Herr',
 'Ritter',
 'wie',
 'gar',
 'meist',
 'Pogner',
 'doch']
In [40]:
len(vokab)
Out[40]:
75

ネットワークグラフの作成

In [41]:
graph = nx.DiGraph()
In [42]:
for i, x in enumerate(vokab):
    graph.add_node(x)
    #graph.nodes[x]['wort'] = vokab[i]
    graph.nodes[x]['freq'] = freq[i]
In [43]:
for x in vokab:
    for y in vokab:
        sim = model.wv.similarity(x, y)
        if sim > 0.15:
            graph.add_edge(x, y, weight=sim)
In [44]:
print(dict(graph.nodes))
{'da': {'freq': 106}, 'kommen': {'freq': 25}, 'geben': {'freq': 33}, 'dort': {'freq': 29}, 'Wort': {'freq': 31}, 'Kind': {'freq': 33}, 'Nun': {'freq': 29}, 'sagen': {'freq': 40}, 'O': {'freq': 20}, 'wohl': {'freq': 77}, 'auch': {'freq': 61}, 'selbst': {'freq': 21}, 'schon': {'freq': 31}, 'Herr': {'freq': 34}, 'Ritter': {'freq': 23}, 'wie': {'freq': 134}, 'gar': {'freq': 63}, 'meist': {'freq': 133}, 'Pogner': {'freq': 9}, 'doch': {'freq': 94}, 'nie': {'freq': 20}, 'Ei': {'freq': 26}, 'Junker': {'freq': 25}, 'nur': {'freq': 85}, 'Gott': {'freq': 32}, 'so': {'freq': 135}, 'jetzt': {'freq': 32}, 'hier': {'freq': 56}, 'weiss': {'freq': 24}, 'David': {'freq': 29}, 'noch': {'freq': 72}, 'Meistersinger': {'freq': 11}, 'preisen': {'freq': 27}, 'dann': {'freq': 30}, 'Lene': {'freq': 20}, 'sehen': {'freq': 22}, 'heut': {'freq': 37}, 'recht': {'freq': 36}, 'lasst': {'freq': 21}, 'wollen': {'freq': 67}, 'soll': {'freq': 29}, 'können': {'freq': 33}, 'neu': {'freq': 21}, 'muss': {'freq': 25}, 'gelingen': {'freq': 24}, 'gelten': {'freq': 20}, 'gehen': {'freq': 27}, 'gut': {'freq': 67}, 'stehen': {'freq': 24}, 'nun': {'freq': 52}, 'machen': {'freq': 47}, 'schustern': {'freq': 25}, 'Merker': {'freq': 24}, 'sollen': {'freq': 25}, 'singen': {'freq': 35}, 'wo': {'freq': 24}, 'Kunst': {'freq': 20}, 'Hans': {'freq': 23}, 'Sachs': {'freq': 60}, 'weit': {'freq': 25}, 'Schuh': {'freq': 35}, 'gleichen': {'freq': 33}, 'denn': {'freq': 26}, 'hoch': {'freq': 26}, 'mögen': {'freq': 33}, 'Herrn': {'freq': 20}, 'Beckmesser': {'freq': 19}, 'Freund': {'freq': 23}, 'hören': {'freq': 36}, 'Lied': {'freq': 50}, 'finden': {'freq': 23}, 'Volk': {'freq': 24}, 'schön': {'freq': 24}, 'Eva': {'freq': 11}, 'Walther': {'freq': 5}}
In [45]:
graph.number_of_nodes()
Out[45]:
75
In [46]:
graph.number_of_edges()
Out[46]:
437
In [47]:
graph.number_of_selfloops()
Out[47]:
75

最短経路

In [48]:
print(list(nx.shortest_path(graph, source='Walther', target='Meistersinger')))
['Walther', 'Gott', 'gut', 'dann', 'Meistersinger']
In [49]:
print(list(nx.shortest_path(graph, source='David', target='Meistersinger')))
['David', 'selbst', 'schustern', 'dann', 'Meistersinger']
In [50]:
print(list(nx.shortest_path(graph, source='Walther', target='Eva')))
['Walther', 'Gott', 'Nun', 'Eva']
In [51]:
print(list(nx.shortest_path(graph, source='Beckmesser', target='Eva')))
['Beckmesser', 'gelten', 'Nun', 'Eva']
In [52]:
print(list(nx.shortest_path(graph, source='Walther', target='Beckmesser')))
['Walther', 'Gott', 'Nun', 'gelten', 'Beckmesser']
In [53]:
print(list(nx.shortest_path(graph, source='Walther', target='Sachs')))
['Walther', 'stehen', 'soll', 'Sachs']
In [54]:
#print(list(nx.all_simple_paths(graph, source='Walther', target='Meistersinger')))

matplotlibで静的グラフを描画

In [55]:
fig = plt.figure(figsize=(16,16), dpi=300)
ax = fig.add_subplot(111)
nx.draw_networkx(graph, with_labels=True, font_weight='bold')
/Users/yuta/anaconda3/lib/python3.7/site-packages/networkx/drawing/nx_pylab.py:579: MatplotlibDeprecationWarning: 
The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.
  if not cb.iterable(width):
/Users/yuta/anaconda3/lib/python3.7/site-packages/networkx/drawing/nx_pylab.py:676: MatplotlibDeprecationWarning: 
The iterable function was deprecated in Matplotlib 3.1 and will be removed in 3.3. Use np.iterable instead.
  if cb.iterable(node_size):  # many node sizes

Bokehでインタラクティヴなグラフを作成

@komo_frによるPEP解析を参考にした。

In [56]:
output_notebook()
Loading BokehJS ...
In [57]:
plot = Plot(plot_width=800, plot_height=800,
            x_range=Range1d(-2,2), y_range=Range1d(-2,2))
plot.title.text = "Wagner2Vek: Ähnlichkeitgraph von \"Der Meistersinger von Nürnberg\" bei Wort2Vek"

node_hover_tool = HoverTool(tooltips=[("freq", "@freq")])
plot.add_tools(node_hover_tool, TapTool(), BoxSelectTool(), BoxZoomTool(), ResetTool(), WheelZoomTool(), PanTool(), SaveTool())
plot.toolbar.active_scroll = plot.select_one(WheelZoomTool)

graph_renderer = from_networkx(graph, nx.spring_layout, scale=1.5, center=(0,0))

graph_renderer.node_renderer.glyph = Circle(size=15, fill_color='#773280', fill_alpha=0.6, line_color='#773280')
graph_renderer.node_renderer.selection_glyph = Circle(size=15, fill_color=Spectral4[2])
graph_renderer.node_renderer.hover_glyph = Circle(size=15, fill_color='firebrick', fill_alpha=0.6, line_color='firebrick')
graph_renderer.node_renderer.glyph.properties_with_values()

graph_renderer.edge_renderer.glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=5)
graph_renderer.edge_renderer.selection_glyph = MultiLine(line_color=Spectral4[2], line_width=5)
graph_renderer.edge_renderer.hover_glyph = MultiLine(line_color='firebrick', line_alpha=0.6, line_width=5)

graph_renderer.selection_policy = NodesAndLinkedEdges()
graph_renderer.inspection_policy = NodesAndLinkedEdges()

plot.renderers.append(graph_renderer)

x, y = zip(*graph_renderer.layout_provider.graph_layout.values())
source = ColumnDataSource({'x': x, 'y': y,
                           'wort': graph_renderer.node_renderer.data_source.data['index']})
labels = LabelSet(x='x', y='y', text='wort', source=source, text_alpha=0.7)

plot.renderers.append(labels)

show(plot)

Next Steps

基本的に会話文であり話者間の方向性があるオペラの歌詞を全て結合して通常のテキストと同様に解析したが、次は話者と相手を考慮して分析・可視化したい。