開け閉め

力強く閉めると反動で数センチ開く

Pythonで偶然短歌をやりたくてMecab使って頑張ったけど…


偶然短歌botというのがある。Wikipediaの記事の中から57577になっている部分を取り出し投稿するbotだ。 たとえばこういうの。

良い。 あるいはこういうの。

もしくはこういうの。

良すぎる……。 こういうのもあった。

最高……!Wikipediaのもつ無機質な文体が、短歌という形で取り出されることによって、荒涼とした侘び寂びのようなものを生み出していて心惹かれるかんじ、たまらん。

……同じことを、やりたい。任意のテキストから短歌を探すやつを、作りたい。

詳しくはわからないが、おそらく形態素解析を用いて単語の音数を抽出し、57577になる部分を抜き取っているのだろう。
形態素解析といえば、こないだの記事はてブコメントで存在を知ったやつだ。日本語を品詞単位で分解するやつ。Pythonでも動くライブラリがあるらしい。

Pythonでできるなら、たぶん、いける。やってみよう。
必要なのはおそらく

  • PythonMeCabをセットアップする
  • 単語の文字数を取得してデータ化する
  • 57577になってる部分を探す

のらへんだろう。

MeCabのセットアップ


文の分析には形態素解析エンジンの中で一番メジャーっぽいMeCabを使う。
MeCabの導入までにはこの記事をほぼまんま参考にした。

また、MeCab標準の辞書だと新語に対応できていないとの話だったので、追加辞書「mecab-ipadic-NEologd」を導入した。参考にしたのはこのサイト。これがかなりしんどかった。このためだけにgitとUbuntuを導入した。泣きそうになった。gitの使い方はこれから勉強します。

f:id:hikido:20190725185223p:plain
Ubuntuを入れるなどした
とりあえずテストでコードを動かしてみる。

import MeCab
t = MeCab.Tagger("Ochasen -d neologd")
print(t.parse(input(">>")))

こうなった。

f:id:hikido:20190725185215p:plain
なるほどね
なるほど確かに文章が分解できている。この分解の単位を「形態素」と呼ぶらしい。
あとは、各形態素ごとの文字数が取得できればいいわけだ。書くぞ!!!!!!!!

import MeCab
import sys

def ParseNode(text):
    p = MeCab.Tagger("-Ochasen -d neologd")
    p.parse("")#なんか消すとおかしくなるらしい
    node = p.parseToNode(text)
    Result = []
    Sentence = []
    while node:
        print(node.feature.split(","))
        word = dict()
        word["text"] = node.surface
        detail = node.feature.split(",")
        word["hinshi"] = detail[0]

        if len(detail)>=9:
            word["Yomi"] = detail[8]
            word["Length"] = len(detail[8])
        else:
            word["Yomi"] = "*"
            word["Length"] = 0

        node = node.next

        if detail[0] == "BOS/EOS":
            if Sentence:
                Result.append(Sentence)
        elif word["Yomi"] == "。":
            Result.append(Sentence)
            Sentence = []
        elif word["hinshi"] == "記号":
            pass
        else:
            Sentence.append(word)
        
        if word["Length"] == "*":word["Length"] = 0
    print(type(node))
    return Result

if __name__ == "__main__":
    t = "".join(sys.stdin.readlines())
    result = ParseNode(t)
    for sentence in result:
        print("\n".join([str(d) for d in sentence]))
        print("------------------------------")

書いた。parseToNodeメソッドの仕様がよくわからなかったので手探りで書いたが、どうも以下のようになっているらしい。

  • 返り値はnodeというものが連結された状態
  • node.surfaceには、単語(形態素)の文字列が入っている
  • node.featureには、単語(形態素)の品詞/品詞の細分類(固有名詞など)/さらなる細分類(人名など)/謎の分類(おそらく細分類の備考)/活用形式/活用形/原型/読み仮名/読みの順にカンマ区切りでデータが入っている
  • node.nextを代入することで、次の形態素に進める

このnodeデータからとりあえず、文字列/品詞/読み/読みの文字数 の4つを取得してリストにまとめた。上記のコードはそのリストを返す関数。

57577探し


形態素に分解したら、それらが5・7・5・7・7音でつながっている部分を探す。松尾芭蕉もこうやって俳句を作っていたのだと思うと、妙な親近感が湧く。 こういう系のアルゴリズムは色々思いつきそうだが、今回は単純な方法で解決しようと思う。すなわち、あらゆる名詞・動詞・連体詞・副詞から形態素の文字数を数えていき、ちょうどよく57577にマッチしたら短歌とみなす。

コードが汚くて恥ずかしい///

from split_node import ParseNode
import MeCab
import itertools
import sys

def FindTanka(text,neologd=False):
    """
    テキストをぶちこむと短歌のリストを返してくれる風流な関数。
    """
    Nodes = ParseNode(text,neologd=neologd)
    TankaPoint = (5,12,17,24,31,32)
    Tankas = []
    for sentence in Nodes:
        l = len(sentence)
        for n,StartWord in enumerate(sentence):
            if StartWord["Yomi"] =="*" or\
            StartWord["Hinshi"] not in ("名詞","動詞","連体詞","副詞"):continue
            sound = 0
            curpos = n
            tanka = ""
            tankalen = 0
            while sound<=31 and curpos<l:
                w = sentence[curpos]
                #句の始まりが助詞や助動詞でないかどうか
                if sound in TankaPoint:
                    if w["Hinshi"] not in ("名詞","動詞","連体詞","副詞"):
                        break       
                if w["Yomi"]!="*":
                    tanka += w["Text"]
                    sound += w["Length"]
                    if sound ==TankaPoint[tankalen]:
                        tankalen+=1
                    if tankalen == 5:
                        Tankas.append(tanka)
                        break
                curpos+=1
    return(Tankas)

if __name__ == "__main__":
    print(FindTanka(wikipedia(input("url:")),neologd = True))

コードは長いが、要するに単語をひとつずつ数えて短歌になっているかどうか調べている。Wikipediaからテキストを抽出する機能もつけた。 これで偶然短歌をぼくも作れるはずだ! とりあえずこのプログラムに青空文庫から「人間失格」をぶちこんだ結果がこれ。 f:id:hikido:20190727004051p:plain 「政党の有名人がこの町に演説に来て自分は下男」 「画家たちは人間という化け物に傷めつけられおびやかされた」あたりは短歌として成り立っていそうだが、その他はどうもしっくりこない。 俳句の終わり際が不自然なので、「体言止め」または「文節の切れ目」で終わっていれば良いはず。

というわけで魔改造した。

明らかに処理に無駄が多い気はするが…

from split_node import ParseNode
from getAozora import aozora
from getWikipedia import wikipedia
import MeCab
import itertools
import sys
import numpy as np
import re

def readlines():
    return " ".join(sys.stdin.readlines())

JIRITSUGO = (
"動詞","形容詞","接続詞",
"名詞","副詞","連体詞","感動詞","記号")#自立後のリスト
FUZOKUGO = ("助詞","助動詞")

def FindTanka(text,neologd=False):
    """
    テキストをぶちこむと短歌のリストを返してくれる風流な関数。
    """
    text =  re.sub(" "," ",re.sub(r"\r"," ",text))
    Nodes = ParseNode(text,neologd=neologd)
    Tankalist = (5,7,5,7,7)#ここのタプルをいじると自由に検出が変更可能
    TankaPoint = np.cumsum(Tankalist+(1,))
    Tankas = []
    for sentence in Nodes:
        l = len(sentence)
        for n,StartWord in enumerate(sentence):
            if StartWord["Yomi"] in("*","、") or StartWord["Hinshi"] in FUZOKUGO:
                continue
            sound = 0
            curpos = n
            tanka = ""
            tankalen = 0
            while sound<=TankaPoint[-1] and curpos<l:
                w = sentence[curpos]
                if w["Hinshi"]=="記号":
                    tanka += w["Text"]
                    curpos += 1
                    continue
                #句(57577)の始まりが助詞や助動詞でないかどうか
                if sound in TankaPoint and tankalen<=4:
                    if w["Hinshi"] in FUZOKUGO:
                        break    
                if w["Yomi"]!="*":
                    tanka += w["Text"]
                    sound += w["Length"]
                    if sound ==TankaPoint[tankalen]:
                        tankalen+=1
                    if tankalen == len(TankaPoint)-1:#短歌が完成した場合
                        if (w["Hinshi"] in JIRITSUGO and "連用" not in w["Katsuyo"])  or (curpos<l-1 and sentence[curpos+1]["Hinshi"] in JIRITSUGO):
                            Tankas.append(tanka)
                            break
                    if tankalen == len(TankaPoint):#31文字でうまく終わらなかった場合の安全策
                        Tankas.append(tanka)
                        break
                curpos+=1
                
    return(Tankas)

if __name__ == "__main__":
    print(FindTanka(wikipedia(input()),neologd=True))

用言の場合は連用形で終わっているものと、直後にまだ助詞/助動詞が続くものを結果から外した。と同時に、字余りの「57578」を許可した。本当はすべての字余りのパターンを許容したかったのだが、コーディングが地獄すぎるので、違和感が少なそうな最後の字余りだけ対応した。 その結果がこちら。

f:id:hikido:20190727004107p:plain
データは同じく人間失格。下から2番めの「言えませんでしょう」の部分などで変化が見られる

だいぶ改善した気がする。とりあえず、短歌を抽出する部分はこのアルゴリズムで良さそうだ。

ちなみに、必死こいてインストールしたipadic-neologd(最新辞書)だが、使うと逆に精度が落ちる場合があるらしい。 たとえば本家の

これ。

80年代にBOØWYのプロモーションの写真撮影を手掛けたことをきっかけに、氷室京介布袋寅泰、吉川晃司、Gacktスピッツなど多くのアーティスト写真を撮影する

という文章からの抽出なのだが、neologd辞書を採用すると短歌として認識してくれない。原因を調べたところ、neologd辞書は「写真撮影」を1語とみなしているため短歌にしてくれなかった事が判明。検出精度がよすぎても短歌的にはマイナスになることがあるんだなあ。 ただ、追加辞書を使わずプリセットの辞書でやると本家のクローンになっちゃうので、neologdは採用することとした。neologdにしか検出できない単語もあるしね。

Wikipediaのデータ注入


本家よろしく、Wikipediaのデータをコイツに流し込む。Wikipediaはサーバーへの負荷を減らすためにクローリングを禁止しているが、そのかわり全テキストデータをまとめたファイルを公開している。そのサイズじつに10GB。もっと重いかと思ったが、画像がないとこんなもんらしい。

これを展開するプログラムがあったのでこれを入手、ちょっと手を加えてさっきのプログラムに放り込む。

オラ!!!!!!!!!!!!!!!
……

……

……



おわびとおわり


プログラムの効率が悪すぎた上にPythonなどという激遅言語で10GBのデータを処理しようとした結果、何時間立っても終わりませんでした…。とりあえず数時間で打ち切ったやつをここにおいておきます。

f:id:hikido:20190727004540p:plain
短歌だけで7MBある。もっと条件厳しくしても良かったのかも

数時間動かしたけど、10メガのファイルサイズは余裕でオーバー。 ざっと目を通した限りだけれども、よさげな短歌はこんな感じか。

f:id:hikido:20190727004656p:plain
パッと見て良さげなやつを出しました。本の名前が短歌になってる
もう同じ趣旨のボットがあるからね、Twtiterには流しませんよ。
ここまでの4文全部短歌です、あえて言うなら必然短歌?





でも辞世の句が 偶然短歌だったら やだな (自由律俳句)