feature image

2021年9月5日 | ブログ記事

【朗報】文才皆無ワイ、AIになろう小説を学習させる

この記事は、traP夏のブログリレー29日目の記事です。

はじめに

こんにちは!

皆さんはめちゃ面白い小説を読んだとき、「おもしろ小説書きて~」と思った事はありませんか?

私は小説最近”三体”という凄まじく面白い小説を読んで「おもしろ小説書きて~」と思い、過去に全く同じことを思った過去の私が勢いそのままに『小説家になろう』へ小説を投稿していたのを思い出しました。

それを読んでみたのですが、めちゃめちゃつまらなかったです。

私には...

私にはおもしろ小説を書けないのか...

じゃあAIに書かせたらええやん!!!!!!!!!!!!!!!!

手法

既存の文章をお手本にして新たに文章を生成する方法としては

等色々ありますが、今回はマルコフ連鎖を用いて実装しようと思います。(最後のほうにチラッとLSTMの実装も置いておきます)
GANも面白そうなのですが、私の理解力が足らず断念しました。

モデルの学習については小説投稿サイト”小説家になろう”から文章をお借りして学習データとします。

出来るだけ意味の分かる文章が作れると良いですね

実装

  1. なろう小説のサイトからスクレイピングによって小説本文を取得し、分かち書きして保存する
  2. モデルに学習させる
  3. 構築したモデルを用いて文章を生成する

といった流れで作っていきます。

実装にはPythonを使用します。外部ライブラリは

を使用しました

この記事で説明するコードは全てこのリポジトリにあります。

小説本文のスクレイピング、分かち書き

モデルの学習毎にスクレイピングをするとサーバーへ負担をかけてしまうので、目次ページURL、ページ数を入力すると分かち書き後、文章単位で改行された本文を保存してくれるスクリプトを作ります。

ソースコードはこちら

# coding: UTF-8
import time
import re
import os
import MeCab
from urllib.request import urlopen
from bs4 import BeautifulSoup

tagger = MeCab.Tagger('-Owakati')
save_folder = 'save'
narou_url = 'https://ncode.syosetu.com'


def make_bsobj(url):
    '''
    引数:(str)対象のURL
    返り値:(BeautifulSoup)BeautifulSoupオブジェクト
    サーバーへの負担軽減の為アクセスの間隔を1秒開けます
    '''
    time.sleep(1)
    return BeautifulSoup(urlopen(url).read().decode(
        'utf-8', 'ignore'), "html.parser")


def format_text(t):
    '''
    引数:(str)テキスト
    返り値:(str)フォーマット後のテキスト
    markovifyが読み込むとエラーを吐き出す文字があるので、それを除去する
    '''
    return re.sub(r'[\._-―─!@#$%^&\-‐|\\*\“()_■×+α※÷⇒—●★☆〇◎◆▼◇△□(:〜~+=)/*&^%$#@!~`){}[]…\[\]\"\'\”\’:;<>?<>〔〕〈〉?、。・,\./『』【】「」→←○《》≪≫\r\u3000\u2000]+', "", t)


def get_bodies_from_url(url):
    '''
    引数:(str)各ページのURL(リゼロならhttps://ncode.syosetu.com/n2267be/1/ など)
    返り値:(str)分かち書き後、文章単位で改行された本文
    '''
    bsobj = make_bsobj(url)
    bodies = ''
    for body in bsobj.findAll("div", {"id": "novel_honbun"})[0].findAll("p"):
        formatted = format_text(body.get_text())
        if formatted == '':
            continue
        bodies += tagger.parse(formatted)+'\n'
    return bodies


def get_num_of_pages(url):
    '''
    引数:(str)目次ページURL(リゼロならhttps://ncode.syosetu.com/n2267be/)
    返り値:(int)ページ数
    '''
    bsobj = make_bsobj(url+'/1')
    return int(bsobj.select('#novel_no')[0].get_text().replace('1/', ''))


def main(url, pages_num):
    id = url[url.strip('/').rfind('/'):len(url)]  # saveフォルダ構成用に識別番号を取得
    all_pages_num = get_num_of_pages(url)  # 全ページ数の取得
    for i in range(1, pages_num+1):
        if i > all_pages_num:
            break
        bodies = get_bodies_from_url(url+'/'+str(i))  # 本文の取得
        folder_path = save_folder+id
        os.makedirs(folder_path, exist_ok=True)  # 保存用フォルダの構成
        with open(folder_path+'/'+str(i)+'.txt', mode="w", encoding='utf-8') as f:
            f.write(bodies)


if __name__ == '__main__':
    url = input(
        "Enter the URL of the novel.\n(ex:https://ncode.syosetu.com/n2267be)\n>")  # url
    pages_num = int(
        input("Enter the number of pages you want to save.\n>"))  # 保存するページ数
    main(url, pages_num)
save_novel.py

を入力し、実行するとsave/n2267be/1.txtのように各ページが保存されます

分かりにくい部分の解説

format_text(t)

get_num_of_pages(url)

全ページ数が格納されている要素

get_bodies_from_url(url)

本文が格納されている要素

では次に、本題のマルコフ連鎖モデルを構築していきます

モデルの構築、文章の生成

ソースコードはこちら

# coding: UTF-8
import save_novel
import os
from glob import glob
import markovify


def main(url, word_size,  out_max, out_min, out_num, first_word):
    id = url[url.strip('/').rfind('/'):len(url)]  # saveフォルダ識別番号
    bodies = ''
    if not os.path.exists(save_novel.save_folder+id):
        print('Path does not exist.')
        exit()
    for file in glob(save_novel.save_folder+id+'/*.txt'):
        with open(file, mode="r", encoding='utf-8') as f:
            bodies += f.read()  # txtファイルを読み込んで結合
    markov_model = markovify.NewlineText(
        bodies, state_size=word_size, well_formed=False)  # マルコフ連鎖モデルを構成
    for _ in range(out_num):
        if(first_word != 'none'):
            try:
                sentence = markov_model.make_sentence_with_start(
                    max_words=out_max, min_words=out_min, tries=100, beginning=first_word).replace(' ', '')  # 文章生成(最初の言葉あり)
            except:
                print(
                    'The word you entered may not exist in the novel.\nOr it may contain unidentifiable characters.')
                exit()
        else:
            sentence = markov_model.make_sentence(
                max_words=out_max, min_words=out_min, tries=100).replace(' ', '')  # 文章生成(ランダム)
        print('out', sentence)


if __name__ == '__main__':
    print('If you haven\'t generated the data for your novel yet, run save_novel.py and run.')
    url = input(
        'Enter the URL of the novel.\n(ex:https://ncode.syosetu.com/n2267be)\n>')
    word_size = int(input(
        'Enter the state history (the more, the closer to the original).\n>'))
    out_max = int(
        input('Enter the maximum length of the text to be generated.\n>'))
    out_min = int(
        input('Enter the minimum length of the text to be generated.\n>'))
    out_num = int(input('Enter the number of texts to be generated.\n>'))
    first_word = input(
        'Enter the first word of the sentence to be generated.To generate randomly, enter "none".\n>')
    main(url, word_size,  out_max, out_min, out_num, first_word)
text_generator.py

を入力して実行すると、パラメータに沿った文章が生成されます

実行中

分かりにくい部分の解説

for file in glob

markovify.NewlineText()

markov_model.make_sentence()

では早速生成していきましょう!!!

生成する

リゼロから生成

俺は進むしかないこれがどんな戦も大事戦う前に勝敗は決まってる

歌詞

俺がいる世界というのもそれが戦闘狂というものがあることは美しきかな

そうかな

相手の素姓が見えているからこそのハリボテの希望なのかも全部

ツイートっぽさがある

いえこっちで連れ帰るわその方がこっちも余計な気をつかわなくていいから

本当の日常会話

無職転生から生成

途中で俺が臭いと一瞬で踵を返してくれるらしいんだ

そう...

ほうれんそうはキッチリと謝っておきたいんだが彼の逆鱗に触れた

律儀だね

暖房が完備されても呪いは消えない

確かに

おまけ(LSTMを使用した方法)

ソースコードはこちら

を使用しています。

保存された本文データをもとにLSTMモデルを構成しているので、マルコフ連鎖モデルと同じように目次ページURLを入力して実行します。

実行中

この様にEpochが進むごとに学習が深まっていきます。

最終的な出力は以下の様になりました(batch_size=128,epochs==30)

これは本気でヤバいと棍棒ってそれはこれ以上に何のかエルザのないでもがを的に頭とかはなんにかていたということだろうがトンとのにものは確認をなかったか今はお前らにてるてるだぜ俺その
異世界にもしかないにも理由だろうかの魔法器はじゃない今のこのては見とそれは無理だぜ頭俺はそのぜで通りをスバルは顔を上げると自分の腰にはとするがたできしかないなかった携帯の画面は
彼女にすればどの面を下げてというところだろうどんな罵声を浴びせだろうともいいだろうが見ているのだがそれを気にした風でもないのが気にしたようなことをあってからすればからにも体を売
っているのだなともちょっとはそうになとはスバルのような気がしてきているのは何度も顔を叩いて気持ちをに二回目の世界のようなのに名前はをそうでフェルトはその名前を呼んだってのことこ
とだけどそれは気をサテラと魔法器だから持っていたなにもがかなかったような気がしてきているのはそれを確認するとをもう今にまでてあっていたそのしていたロム爺は聖金貨の二人が全てはそ
の自分の手を当てて後ろに向けてが蔵の中にていると思うが今のこのてに見ともはないしはけどないのかなのによなエルザの声をロム爺の言葉を笑みはこっちスバルて顔なかっでのないエルザはそ
の刃をするとでエルザにロム爺の出したミルクの入ったグラスを傾けるフェルト彼女の赤い瞳からは警戒心

めちゃめちゃですね


なろう小説を自動生成するという目的だとこちらの方が一見良さそうに見えますが、マルコフ連鎖モデルに比べて幾つかの短所があります

1. 構築に時間がかかる

tensorflow-gpuを使用し(RTX3080Ti)、25000単語ので構成したモデルの学習が30Epochs終わるのに3分程度かかっています

これは私のLSTMの実装が簡素なものである事を考慮してもマルコフ連鎖モデルに比べて遅いです

マルコフ連鎖モデルは同じ文章量を0.1sec以下で学習できます

2. メモリが足りない

私の実装では単語数を25000語に制限しています。もしリゼロ50話分(185391語)でモデルを構築した場合

numpy.core._exceptions.MemoryError: Unable to allocate 160. GiB for an array with shape (185386, 5, 185391) and data type bool

とNumpy配列でMemoryErrorが出ます。
これはより効率的な実装をすることで回避できそうな気はします....

まとめ

思ったより文章っぽい文章が生成出来て満足です

みんなもマルコフ連鎖を使ってなろう小説を書こう!

明日は@d_etteiu8383さんの記事です。お楽しみに!

Renard icon
この記事を書いた人
Renard

20B 絵とゲームをたまに作ります

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年4月18日
ベズー係数とN項の拡張ユークリッドの互除法
0214sh7 icon 0214sh7
2017年11月4日
文章をよしなに分散表現しよう
David icon David
2021年9月8日
五度圏⊃自然音階って…コト!?
kotoki_bis icon kotoki_bis
2021年9月3日
部活青春系エロゲで涙腺崩壊した話
mera icon mera
2021年8月30日
【夏休み自由研究】実例で学ぶ画像処理【Python】
d_etteiu8383 icon d_etteiu8383
記事一覧 タグ一覧 Google アナリティクスについて