洞窟の比喩

μετὰ ταῦτα δή, εἶπον, ἀπείκασον τοιούτῳ πάθει τὴν ἡμετέραν φύσιν παιδείας τε πέρι καὶ ἀπαιδευσίας. ἰδὲ γὰρ ἀνθρώπους οἷον ἐν καταγείῳ οἰκήσει σπηλαιώδει, ἀναπεπταμένην πρὸς τὸ φῶς τὴν εἴσοδον ἐχούσῃ μακρὰν παρὰ πᾶν τὸ σπήλαιον, ἐν ταύτῃ ἐκ παίδων ὄντας ἐν

【再掲】サカモトツイートジェネレータ

これは「みす 52nd Advent Calendar 2019」19日目の記事になります。

三度目まして、52代CG研究会のΙΔΈΑです。あ、公式アドカレを含めれば四度目ですね。今回はプログラミング研究会っぽい話をします。

ずばりなにを作ったのかというと、それはサカモトツイートジェネレータです。名のとおりサカモトさんのツイートをジェネレートするというものです。なにを言っているのかわからないかもしれませんが、とりあえずまあ適当に読んでみてください。

f:id:idea_misw:20201102155003p:plain

プログラミング研究会の方へ

所詮はCG研究会の私が書く記事なので、間違ったことや変なことがあるかもしれません(きっとあるでしょう)。もしもなにかおかしなことを見つけた場合は、なにも見なかったことにするか私に優しく教えてください……

それとコードの書き方が下手だとかコードにセンスがないとか、仮に思ったとしてもそれはそっと胸の内にしまっておいていただけると幸いです……

動機

皆様はサカモトカレンダーをご存知でしょうか。一言でいえば「サカモトになりきる」という企画です。非常に興味深い企画なので、ぜひ一度ご覧になってみてください。

adventar.org

先日私のもとにも参加のお誘いがあったのですが、私にサカモトさんのような文章を書く自信はなかったのです。そして悩みに悩んだ私が最終的に辿り着いた発想は「もう自動で生成すればよいのではないか」というものでした。

ちょうどそのとき発表会の予備日が数日後に控えていました。提出される作品の数は多くないと聞いていたので、それを発表できたらきっと楽しいだろうなと思ったのです。

そういうわけで今回の計画は開始されたのでした。

経緯

……とはいっても自動で文を生成するだなんてかなり難しそうで、初めはほんの軽い冗談のつもりでした。

物は試しに調べてみようかと「文 自動 生成」みたいな感じに検索し、私は驚くことになります。

「そんなに難しくなさそう」

多くある手法のうちもっとも簡単なものは私にでも作れそうな難易度でした。作れそうならば作るしかないですよね。このときから本格的な開発が始まりました。

技術

今回の実装における材料は次の二つで、それぞれ簡単に解説してみます。

形態素解析

難解な五字熟語に聞こえるかもしれませんが、これはつまり文における形態素を解析することです。文字のとおりですね。

形態素というのは文を構成する最小単位のようなもので、単語と少し似た概念です。正確にいうと形態素の方が低いレイヤにあり、単語はさらに形態素へ分解されうるようです。

このように与えられた自然言語を小さな単位に分けてあげることで、機械はそれを上手に処理できるようになるのです。

$ mecab
母にハンネで呼ばれたのでスマホ食った
母 名詞,一般,*,*,*,*,母,ハハ,ハハ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
ハンネ 名詞,一般,*,*,*,*,*
で 助詞,格助詞,一般,*,*,*,で,デ,デ
呼ば 動詞,自立,*,*,五段・バ行,未然形,呼ぶ,ヨバ,ヨバ
れ 動詞,接尾,*,*,一段,連用形,れる,レ,レ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
ので 助詞,接続助詞,*,*,*,*,ので,ノデ,ノデ
スマ 名詞,固有名詞,一般,*,*,*,スマ,スマ,スマ
ホ 名詞,一般,*,*,*,*,ホ,ホ,ホ
食っ 動詞,自立,*,*,五段・ワ行促音便,連用タ接続,食う,クッ,クッ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS

私の所属している学科に研究室の仮配属みたいなものがあり、私はとある研究室で自然言語処理を用いた簡易的な研究に取り組んでいるところで、ちょうどこの形態素解析を学んだところだったのです。それを使わない手などありませんでした。

なおその簡易研究ではMeCabというエンジンを使ってこれを行っているのですが、今回はより簡単に使うことができる別のJanomeというエンジンを使ってみました。

taku910.github.io

mocobeta.github.io

マルコフ連鎖

これはいわゆるマルコフ過程のことで、現状態からのみ次状態が決まるようなマルコフ過程のうち状態が離散的なものをマルコフ連鎖というそうです。せっかくなので覚えておきましょうね。

例えば下に示す図(シャノン線図)は単純マルコフ連鎖を表しており、現状態がaのとき次状態が0.15の確率でa、0.65の確率でb、0.20の確率でcとなるのです。あ、この例に意味はありません。

f:id:idea_misw:20201102155018p:plain

これを文の解析に適用すれば、たくさんの文をもとにして新たな文を生成することができるようになります。具体的にいうと、過去のツイートたちから形態素マルコフ連鎖を構築することにである言葉の次に現れやすい言葉を学び、今度はそれをもとに言葉を並べて新たに文を構築するということです。ね、難しくないでしょ?

ちなみに私のいる学科や隣の学科に所属している皆様は三年春期の選択科目でマルコフ連鎖について詳しく学べると思います。

実際

そんなこんなで私が書いたコードを載せておきます。は、恥ずかしいのであまりジロジロ見ないでくださいね……

import csv
import random

from janome.tokenizer import Tokenizer

TWEET_LEN = 140
GEN_NUM = 100

CTL_CHAR = '\\'

def read_tweets(filename):
    tweets = []
    with open(filename, encoding='utf_8') as f:
        reader = csv.reader(f)
        header = next(reader)
        for row in reader:
            tweets.append(row[0])
    return tweets

def write_tweets(tweets, filename):
    with open(filename, 'w', encoding='utf_8', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['generated tweet'])
    for tweet in tweets:
        writer.writerow([tweet])

def tokenize_tweets(tweets):
    t = Tokenizer()
    return [t.tokenize(tweet, wakati=True) for tweet in tweets]

def generate_chain(tweets, chain_num):
    chains = {}
    for tweet in tweets:
        tweet = [CTL_CHAR] + tweet + [CTL_CHAR]
        chain = tuple('' for i in range(chain_num))
        for element in tweet:
            if '' not in chain:
                if chain not in chains:
                    chains[chain] = []
                chains[chain].append(element)
            chain = chain[1:] + (element,)
    return chains

def generate_tweet(chains):
    init_prefixes = [p for p in chains.keys() if p[0] == CTL_CHAR]
    prefix = random.choice(init_prefixes)
    tweet = ''.join(prefix)
    char_ct = 0
    while char_ct < TWEET_LEN + 2:
        if prefix not in chains:
            init_prefixes = [p for p in chains.keys() if p[0] != CTL_CHAR]
            prefix = random.choice(init_prefixes)
        element = random.choice(chains[prefix])
        if element == CTL_CHAR:
            break
        char_ct += len(element)
        if char_ct > TWEET_LEN + 1:
            break
        tweet += element
        prefix = prefix[1:] + (element,)
    return tweet[1:]

tweets = read_tweets('sakamoto_tweets.csv')

tweets_tk = tokenize_tweets(tweets)
# tweets_ch = [list(tweet) for tweet in tweets]

chains_tk2 = generate_chain(tweets_tk, 2)
# chains_ch3 = generate_chain(tweets_ch, 3)

gen_tweets_tk2 = [generate_tweet(chains_tk2) for i in range(GEN_NUM)]
# gen_tweets_ch3 = [generate_tweet(chains_ch3) for i in range(GEN_NUM)]

for i in range(GEN_NUM):
    print(gen_tweets_tk2[i])
    # print(gen_tweets_ch3[i])

なぜか過去のツイートをcsvファイルで読み込んでいますが、おそらくデータベースとかに突っ込んでおいた方が後々嬉しいですよね。勉強してきます。

それとマルコフ連鎖形態素の単位でなく文字の単位で試みた跡がありますが、これは結局ボツになりました。

ちなみにもうお気づきかもしれませんが、あくまでも上のコードは入力した大量の文に対してそれっぽい文を出力するというものであり、実はただのツイートジェネレータなんですよね。結局サカモトツイートジェネレータとして重要なものは入力のcsvファイルであって、本質はそこにあるのです。

成果

書いたコードを実行して得られた文のうち、成功したものと成功しなかったものを適当にピックアップして紹介しようと思います。

うまくいったもの

醤油の池を作ることでハッピーヒューマンになった

得意ジャンル振られると踊っちゃうな

ざむちゃんを見つけるのがおわりのはじまり

ご飯食べながらアイカツ見ちゃお

ミニトマトゼリーというものを食べたのかわよ……って言ってる

黄色ヨッシーになりました

オリンピック正式種目である可能性、アリ

色んな方が自分を見る、アマプラで

パルキアディアルガに挟まれた秋

ダメだったもの

かわいい〜〜!!!誰よその女

これは文脈が一貫していないような例で、過去をあまり考慮しないマルコフ連鎖による文の生成で起こりやすい現象です。ただしこれはこれでまた面白いんですよね……

今更更新見てなるほどなぁになったことないのでクラスを見渡してもずっと好きで ああ あなたにも載ってて「ヤダ!!!」と言われているだけのリア垢()が知人と繋がったのでCプロの存在、もっと早く知りたかっただけだろー!と言おうとして私の画集は出せそうな予感がしました!脳がバグる可能性がありますが……ッッ!

これは偶然文が長くなってしまいもはや意味が伝わらなくなってしまった例ですね。生成については140字以下という制約しか課していないため、このような文が生まれるケースもあります。

切られたサイコロ

これは上記の例と反対に、文が短すぎてしまったパターンです。ちなみに私はサイコロを切ったことはありません。

涼しい!きっとこの電車はエンターテインメント」となった

一見すると内容に問題はなさそうですが、文の途中に不適切な記号(」のこと)が入ってしまっています。これもなにか対策を施して避ける必要があるでしょう。

くにしちゃおっかな

おそらく「くにしちゃん」と「〇〇しちゃおっかな」が混ざって生まれた文です。これには私も思わず噴き出してしまいました。

ざむちゃんとは寿司の契り交わしました

なにひとつ問題のない文に見えますが、実はこれは過去のツイートとして現に存在しているのです。私が書いたコードの性質上、学習に使用した文章がそのまま出力されてしまうことがあるのです。要対策ですね。

今後

いかんせん突貫開発だったので、挙げられる問題点は山のようにあります。

例えばマルコフ連鎖による実装は高々数個の単語から次に現れそうな単語を予測するものであり、遠くにある単語同士の文脈関係(すなわち文における意味的な一貫性)を保証しません。また構文的な正しさに関しても必ず保たれるというわけではありません。

また異なるアプローチとして、今話題沸騰中のニューラルネットワークを活用するというものがあります。RNNであるとかLSTMであるとか、調べてみたらいろいろと見つかったので次はこれに挑戦してみてもよいかもしれません。

ちなみにもともと別の案で「サカモトツイートディスクリミネータ」というものがありました。これは与えられた文がサカモトさんによるものなのか否かを判別するツールで、対サカモトカレンダー用として思いついたものでした。これも調べてみたら方法がさまざまにありそうなので、今度はこの立場へと移ってみるのも選択肢ですね。

あとは今回の生成器をTwiterのbotとかにしてみると面白そうですよね。botと本人のやりとりが生まれたりすれば非常に興味深いような気がします。ひとまずのところはこれに取り組むのが最優先かなあ、と。

後書

ずっと文の生成とか簡単な自然言語処理みたいなことをやってみたいと思っていて、ずっとタイミングを見つけられずにいたんですよね、私。

プロジェクト研究と発表会予備日、そしてサカモトカレンダーの存在がそんな私を動かしたのです。

つまりわずかではありますが、これらが私の夢を叶えてくれたことになります(発表会の発表こそ叶いませんでしたが)。実に不思議な話ですね。本当にありがとうございました。

そんなわけで、これからも適度に暇を見つけて開発を続けていきたい所存であります。いつか皆様が簡単に楽しめるような形でこれを提供することができたらいいな、と。

そしてサカモトカレンダーも残すところあと1週間となりました。最後まで決して油断することなく、しっかりと本物のサカモトさんを見極めていきましょう。

adventar.org

以上、52代CG研究会のΙΔΈΑでした。さて、明日は誰の記事となるのでしょうか。

楽しみですね。

参考

ほかにもいろいろと参考にしたような気がするので、もしかしたら適宜更更新するかもしれません。

おまけ

発表会予備日で使うはずだったスライドを置いておきます。

わざわざスライドを作らなければ発表会に間に合っていたという噂もあるのですが、結局スライドが無ければ発表は成り立たないと思うのでどうしようもなかったんですよね、ジッサイ。