gpt-2をつかって、なろう小説を書いてみよう！

自然言語処理でおなじみ、gpt-2!
それが、[Rinna](https://github.com/rinnakk/japanese-pretrained-models)のgithubで公開されていることはもう知っていますよね。


今回はこのありがたいgpt-2のモデルを使って、お気に入りの作家さんの文章を生成してみたいと思います！

###やり方

1.[小説家になろう](https://syosetu.com/)から、お気に入りの作家さんのデータをテキスト形式(.txt)で保存する。
2.Rinnaのgithubから、gpt-2をcloneして、いろいろと調節をする。
3.実際に学習させて、文章を生成してみる。

では早速1から始めていきたいと思います。

##1　「小説家になろう」からのtextの抜き出し。

今回はrequestsとbeautifulsoupを使って、scrapingをします。

##注意！

\----------------------------------------------------------------------------------------------------------------------------------------------
必ず、今回のScrapingした結果は個人利用にとどめておいてください。
良識をもって、というよりヲタクとしての矜持をもち、作者様に敬意を払って、Scrapingさせていただきましょう。
\----------------------------------------------------------------------------------------------------------------------

今回Scrapingさせていただく作品は

<details><summary>[**『願わくばこの手に幸福を』**](https://ncode.syosetu.com/n3170ed/)</summary>
~\~\~あらすじ~\~\~
救世の旅に同行を続けるルーギス。
その中で彼の役割は、雑用や夜番。
女達からは疎まれ、冷たい視線を与えられる日々。
だが幼馴染のアリュエノの存在だけが、彼をパーティに繋ぎ止めていた。
例え、彼女が救世者と呼ばれる男に心奪われていたとしても。
失意と惰性の日々。
そんな中に訪れた影は告げる。
「貴様に機会を与えてやろう。全てを塗りつぶし、人生という絵画を描き直す機会を！」</details>

という作品です。めちゃくちゃ面白いので、時間があれば是非に！

それでは、小説家になろうからtextを抜き出すコードです。

```
import requests
from bs4 import BeautifulSoup
from tqdm import tqdm
from time import sleep

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36'}
url_header = 'https://ncode.syosetu.com'

def get_text(url, gyokan=False):
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    text = soup.find(id='novel_honbun')
    texts = [line.text for line in text.find_all('p')]
    texts = [line.replace('\u3000', '') for line in texts]
    if not gyokan:
        texts = [line for line in texts if line != '']
    return texts

def get_page(ncode='n3170ed', gyokan=False):
    url =  url_header+ '/' + ncode + '/'
    print(f'url : {url}')
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    print(soup)
    indices = soup.find(class_='index_box')
    if indices == None:
        lines = get_text(url, gyokan)
        with open(ncode + '.txt', 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines))
    else:
        hrefs = [a['href'] for a in indices.find_all('a')]
        hrefs = sorted(hrefs, key=lambda x: int(x.split('/')[-2]))
        print(f'length of hrefs {len(hrefs)}')
        for href in tqdm(hrefs):
            print(f'Start {href}')
            url = url_header + href
            lines = get_text(url, gyokan)
            sleep(2.0)
            with open(ncode + '.txt', 'a', encoding='utf-8') as f:
                f.write('\n'.join(lines))



if __name__=='__main__':
    get_page(ncode='n3170ed', gyokan=False)
```

実際に使用する際は
```
if __name__=='__main__':
     get_page(小説のNコード, 行間を含む場合はTrue)
```
を書き換えて実行してください。

一応、連載小説でも短編小説でもScrapingできるようになっています。

行間についてですが、gpt-2の学習データ作成の際に空行ごとに区切って、データをまとめて作るので、おすすめはワンシーンごとに行間を入れる感じです。

もし、短編等で短いデータしか得られない場合はgyoukan=Trueとした方がいいかもしれません。

(短編は学習させてないので不明です。)

##2 GPT-2の設定

次にrinnaのgithubからcloneしたgpt-2の設定をしていきたいと思います。

まず、初めにPCの実行環境は以下のようなものになります。

OS: Windows 10 Home
CPU: AMD Ryzen 7 3700X 8-Core Processor
RAM: 16.0 GB
GPU: Nvidia GeForce RTX 3070

この環境ですが、普通にやるとMemory Out だのRuntimeError: CUDA out of memory.だのに普通に引っ掛かります。

何なんだよ。GPUが4台とか、工学系の研究室だとあり得んのだよ(´;ω;｀)

rinna のgpt-2は[Hagging Face](https://huggingface.co/)の gpt-2 mediumがベースとなっているのですが、mediumは基本的にGPUが4台であることが条件なので、普通にGPUが一台では動きません。

なので、gpt-2 small のモデルを使って学習させます。

また、RAMが16.0 GBしかないので、そのままだとデータバッチが作れません。

ちょっとずつ変えていきます。

####1.gpt-2 smallへの変更。

pratain フォルダーのtrain.py関数を変えていきます。

まず、--model_config_file_pathのpathをgpt2-ja-medium-config.jsonから、gpt2-ja-small-config.jsonに変更します。

また、--model_sizeをmediumから、samllに変更します。

また、--batch_sizeと--eval_batch_sizeを1に変更します。

これで、RuntimeError: CUDA out of memory.にならなくなります。

####2.Data Batchの作成

このままだと、
```
           with mp.Pool(processes=config.n_train_files_per_group) as pool:
                group_train_docs = pool.starmap(
                    load_docs_from_filepath,
                    [(train_filepath, tokenizer) for train_filepath in group_train_filepaths]
                )
```
の部分でMemory Outになります。
もし、Memory　Outにならないよって人だったら、ここは飛ばしてかまいません。

Memory OUTになる人は--n_train_files_per_group のdefaultを10から5に変更します。

また、次に作る学習データで記号ばかりの行が存在した場合、その行を消去してください。

####3.gpt2-ja-???-config.jsonの編集

そのままだと、大量にエラーが発生するのでそれぞれのファイルに以下の行を追加してください。

```gpt2-ja-medium-config.json

    "hidden_size": 1024,
    "max_position_embeddings": 1024,
    "num_hidden_layers": 24,
    "num_attention_heads": 16,
    "scale_attn_weights": true,
	"use_cache": false
```


```gpt2-ja-small-config.json
    "scale_attn_weights": true,
	"use_cache": false,
	"hidden_size": 768,
    "max_position_embeddings": 1024,
    "num_hidden_layers": 12,
    "num_attention_heads": 12
```

あとは、学習データを作れば動くようになります。

####4.学習データの作成。

jp_cc100フォルダーにあるsplit_to_small_files.pyを使用します。

まず、小説家になろうでScrapingしたデータの名前をja.txtに変更します。

つぎに、エディターの置換機能などを使って、今回の場合ですと、**＊**や**◇◆◇◆**などのシーン区切りのところを空行に変更します。

次にそのデータをjapanese-gpt2-master/japanese-gpt2-master/data/jp_cc100/raw_data/の中に突っ込んで、split_to_small_files.pyを実行します。
すると、doc_dataに分割されたtxtファイルができます。

ここで、注意することはファイル数が42以上あるか確認してください。

もし、42より少ない場合はsplit_to_small_files.pyのN_LINES_PER_FILEを調節して、ファイル数が42以上になるようにしてください。

または、train.pyのdev_file_idx = 42の42をdoc_dataに作られたファイル数よりも少ない数に変更してください。

##3.gpt-2の学習

準備がこれで完了したので早速学習させます。

学習すると、pretrainフォルダー内にcheckpoint fileが生成されるので、これを使って文章を生成します。

interact.pyを開いて、

まず、--model_config_filepathをgpt2-ja-small-config.jsonのpathに変更します。

次に、--checkpoint_pathにdefault=/path/to/gpt2-ja-small.seed_42.???.best.checkpointを追加します。

これでgpt-2で文章が生成できるようになりました。

実際の結果を表示しておきます。

>Prompt: 願わくば
Generated text: 良い。 何があの英雄は、そんな中を這いていくように口を動かした、血を掴みをあげていることを拉げた。俺はしない。 あああ、私は引きの白に笑み合いからその身を殺す怨念を覚悟してやりたい。 「
お前ならそれで。俺です。この所で、友間抜けてくれよ」 ルーギスを殺すネイマールは、魔人と相対しているからこそ弱い事。とてもではないが。それ以外の事も、結構わないと、何がいつと、こうと思われない。お前ら
しい。あれは相手をマシだって願いさないのは、エルフはただ守るべき表情を見てまわれば。それなら、その存在を欲しさとんだ。 フィアラート=ラブールを踏みつけられた。 「......近しいの重みれば良いわ、といえ。
だが――英雄殿だっていうように震えて、ドブネズミのなら、今日は自らの手に曲げて、
        (response generation took 2.375 sec)
Prompt: カリアが
Generated text: 受け入れられないというのであれば、途中で顎を抜いていったようにはない。僅かに響き方の精神が、青ざめながらも、なんてもの。エルフの巨獣のが見えた。王宮がたいようなものは、如何にも計に、
その馬が、砂石のは、研究は手も、白く編み込まれた、今火花の上へと変山そのもの。 身体の揺れ動がヘルト、波に、何処までも、鎖を揺らす。夜の中、ただ、何時だ。何処か落ちてくる。 そうだった、後目が、今には
、俺の、俺も。 ――———————不思議なことは何が、手の中を揺れる。場に逸らと。俺の刃をかき消しながら跳ぶれた。白刃は、その意志を示しただけで、全身を、身体の中、そう泥を衝突しばされたかのように、魔剣が、
その銀の首筋の意味もなく
        (response generation took 1.750 sec)
Prompt: ルーギスの
Generated text: 人間者も、その道の確認するものでありながら。少なくとも、ルーギスが膝で、そのの喜びとも有り得なかった。その考えは、頭がしないように感情を押し殺す。 全く、紛れもないそれとも美しいことも
ない、未だに暗い手元よりの首筋に、ただ、足幕に逃げろ布があった。無礼する。ただ、息を、影を、さぬと、宝剣を裂かれる瞼元では、其処は否を、もう、運命が。 白い鼻首筋が、ガルラスの如くた。 リチャードの白
刃にて遮ろうとしている。それだけの如く。敵の音が鳴ったような、真面白刃だった。そう呼ばれる行動。その比較の刃に引きず、もはや、ガルガンティアに焼け焦が知らず瞼の裏にすらした。未だ刃が、僅かな。 かつて
、到底聖女と騒うねと、何故何故弱い者など強い。
        (response generation took 1.750 sec)
Prompt: 死してなお
Generated text: ない。俺の石板を見進めばった声だった。何とも理通りで何せかを知るものではないさねばならない人間は理解していた気がルーギスの姿も随分、そう思いどれ程がある様ならんと思う。 じっくりと<unk
> く――そうして動く気だ。何時かって、何かしらの発したからとも違うが動きは男の言葉に塗れれば、こうして、そのままに注きっと。勿論ない。 かつて人間のこのような未来を僕は自嘲するように、彼らは、耳元にあ
の日、かつてこの国官が耳に入った。 「おしいとはいえました」 ならば、思い出していたの瞳が固くつかせた。何時まで消え失う。背信者が、何よりも随分と溢れんだと、ただ、俺の顔下らない。だが、何度どんな声だ
った。 こんですら失った暮らしの奪還された。 では、声でだろうか。 そうとも、それも
        (response generation took 1.734 sec)
Prompt: 悍ましい
Generated text: 。眼には冷え。 「だから、あんたは、こいつ様。あ、見習い魔力が攻め始めていた。そう、宝剣技であっても勝て才能策として生きる。なるだけの事がある。普通、お前のよ。そのはそれを聞いて、いず
れ人である限りあば何とかなのか!」 剣を吐息絶える数度、死ぬの瞳。そんな身創<unk> 悩すら、斬り殺せない、リチャードのだろう、俺の地面を、俺の切っているの原典が、宝剣を漏ら綺麗に、その剣は、今までは真っ
すぐに、舌を立てて、不思議と、かき鳴く。その時。 血に、周囲へと赴いていた。それも、俺自身に耳<unk> き、背後を焼き尽くさんとした。 宝剣呑みして似つかみとも奇妙な音が吹き飛沫が耳<unk> のようなものすら
覚えていた。血が鉄が肉底から手首を刈りのは、片もドリグマンは
        (response generation took 1.750 sec)


なんとなく、雰囲気が伝わる文章になりました。

ぜひ、自分の好きな作家さんでgpt-2を試してみてください！