青空文庫の小説の表紙をAIで自動生成してみた

# 概要

## はじめに

これは [富士通クラウドテクノロジーズ株式会社 Advent calendar 2022](https://qiita.com/advent-calendar/2022/fjct) の10日目の記事になります。
昨日は @nori3636 さんで[wslでminikubeの環境構築をする話](https://qiita.com/nori3636/items/f4287be438d98b62152a) とでした。
Kubernetesは自分も少し触った程度でまだ使いこなせていないので、自分も環境を構築していろいろ試してみたくなりましたね。

## 本題

皆さん、人はふとしたときに本を読みたくなるものですが、明るい内容だったり暗い内容だったり**その時の気分によって読みたい本の傾向は変わって**きますよね。

書店にあなたが足を運んだならば、きっと**店頭に並んでいる本の表紙を見て**気になる本を見つけることでしょう。

しかし、ネット上でフリーで公開されている **青空文庫の書籍には表紙がありません**。気になった本をジャケ買いならぬ、ジャケ読みすることもできないのです。

だったら**小説の内容から自動で本表紙が作成できる**ようにしましょう。

## 作ったもの

人類がぱっと見で内容を理解するためにはやはり、**本の内容を図示**する必要があります。
そのため、書籍のテキストデータから頻出単語を抽出して**text-to-imageの深層学習モデル**を使ってその書籍のイメージ画像を生成しました。

結果として「人間失格」では強気な女性が、「人間椅子」では椅子に腰かける女性がそれぞれ出力されました。
**あなたが今読みたいのはどちらの本ですか？**
![人間失格.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/5fe703fc-8cef-7066-1be3-1a0a377e1730.png)![人間椅子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/f2285181-199b-c189-5164-cd98424c05c3.png)

# 実装

## 作業環境

Google Colaboratory

## 必要なライブラリのインストール

```
!pip install accelerate
!pip install wordcloud spacy SudachiPy ja-ginza
!pip install git+https://github.com/rinnakk/japanese-stable-diffusion
```

## pythonライブラリのインポート

```
import urllib.request
from bs4 import BeautifulSoup
import re
import regex
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from wordcloud import WordCloud
import spacy
from sudachipy import tokenizer
from sudachipy import dictionary

import torch
from torch import autocast
from diffusers import LMSDiscreteScheduler
from japanese_stable_diffusion import JapaneseStableDiffusionPipeline
```

## 関数の実装

### 青空文庫からのテキスト抽出

こちらは [過去に自分が作ったもの](https://qiita.com/e10persona/items/7a7643b266c2bdfbf7d0#%E9%9D%92%E7%A9%BA%E6%96%87%E5%BA%AB%E3%81%AE%E4%BD%9C%E5%93%81%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%96%E5%BE%97)を流用
青空文庫の書籍のURLを与えると、そのテキスト情報だけを前処理して返してくれます

```python
def get_txt_from_aozorabunko(url):
  html = urllib.request.urlopen(url=url)
  soup = BeautifulSoup(html, "html.parser")
  sentences = soup.find("div","main_text") # 本文を取得する → <div class="main_text">～本文～</div>
  sentences = sentences.get_text().replace("\r", "").replace("\n", "").replace("\u3000", "") # 文字部分のみを抽出する
  sentences = re.sub("（.*?）", "", sentences) # 全角の括弧に囲われた文字と括弧を除去（ルビが括弧文字として存在するため）
  return sentences
```

### テキストデータからの頻出単語抽出

今回は頻出単語として抽出する品詞を「**名詞、形容詞、形容動詞**」に限定しました。
また、表紙の生成にあたりキーワードとなる単語が多いほうがある程度の複雑さが生じるのではないかという仮説から、**1つの小説当たり30個の単語を抽出する**ようにしています。

書籍の中の文章をそのまま分かち書きすることはできないため、一旦読点（「。」）ごとにテキストを分割して処理を実施しています。

```python
def extract_words(text, nlp, tokenizer_obj, mode):
    p = regex.compile(r'\p{Block=Hiragana}{1,2}') # ひらがな2文字以下で構成されている文字列を指定する正規表現
    words = []
    doc = nlp(text)
    
    for sent in doc.sents:
        for token in sent:
            if p.fullmatch(token.lemma_):
                # ひらがな2文字以下の単語の除去
                continue
            if token.pos_ in ["NOUN", "ADJ", "ADV"]:
                word = token.lemma_
                word = tokenizer_obj.tokenize(noun, mode)[0].normalized_form() # 単語の正規化
                words.append(noun)
    if words == []:
        return None
    return ' '.join(words)

def preprocess_sentences(sentences):
  list_sentence = sentences.split("。")
  list_processed_sentence = []

  nlp = spacy.load('ja_ginza')
  tokenizer_obj = dictionary.Dictionary().create()
  mode = tokenizer.Tokenizer.SplitMode.C

  for sentence in list_sentence:
    words = extract_words(sentence, nlp, tokenizer_obj, mode)
    if words == None:
      pass
    else:
      list_processed_sentence.append(words)
  return " ".join(list_processed_sentence)

def extract_keywords_from_sentences(sentences):
  keywords = WordCloud(collocations = False, # 複合語のオプションをオフ
            random_state=1).process_text(sentences)
  return keywords

def get_frequent_words(url):
  sentences = get_txt_from_aozorabunko(url)
  nouns = preprocess_sentences(sentences)
  keywords = extract_keywords_from_sentences(nouns)
  df = pd.DataFrame.from_dict(keywords, orient='index').reset_index()
  df.columns = ["keyword", "frequency"]
  df = df.sort_values("frequency", ascending=False)
  words = df.head(30)["keyword"].values
  words = " ".join(words)
  return words

```

### 頻出単語情報からの表紙画像生成

#### Hugging Faceへのログイン

今回はtext-to-imageモデルとして日本語のキーワードが入力可能なHugging Faceで提供されている、[日本語版stable-diffusion](https://github.com/rinnakk/japanese-stable-diffusion)を利用しました。利用にあたり環境での認証が必要なので実行します。

```
# Hugging Face へのログイン　ユーザー登録が必要
!huggingface-cli login
```

実行すると以下のようにTokenの入力を求められるのでリンク先でユーザー登録とログインをしてTokenを取得。一度登録を済ませれば以降は同じTokenで認証が可能なようです。

```

    _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|
    _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|
    _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|
    _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|
    _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|

    To login, `huggingface_hub` now requires a token generated from https://huggingface.co/settings/tokens .
    
Token: 
```

#### 関数作成

これで実行環境の準備は整ったので関数を作成します。この際、`keywords`は先ほどまでの関数群によってい生成されている想定です。
今回は表紙を抽象的な絵画のようにしてほしいなと思っているので、わざと入力する文字列の頭に`抽象画`という文字列を追加しています。
また、ColabでGPUの機能をONにしていないとこちらの関数の実行時にエラーが生じます（~~忘れててcuda用のデバイスが無いぞって言われました~~）。

```python
def setup_model():
  model_id = "rinna/japanese-stable-diffusion"
  device = "cuda"
  # Use the K-LMS scheduler here instead
  scheduler = LMSDiscreteScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000)
  pipe = JapaneseStableDiffusionPipeline.from_pretrained(model_id, scheduler=scheduler, use_auth_token=True)
  pipe = pipe.to(device)
  return pipe

def generate_cover(pipe, keywords):
  keywords = "抽象画 " + keywords
  with autocast("cuda"):
      image = pipe(keywords, guidance_scale=7.5).images[0]
  return image
```

また、余談ですがjapanese-stable-diffusion公式のUsageには以下のようにサンプル画像を参照できるように記載がありますが、

```python
image = pipe(prompt, guidance_scale=7.5)["sample"][0] 
```
上記を実行するとそんなものは無いとエラーを吐いてくるので、
```python
image = pipe(keywords, guidance_scale=7.5).images[0]
```
正しくは上記のようにすることで画像を参照できます。

### 実行

これで準備は整いました。あとは実行していくだけです。

```python
pipe = setup_model()# stable-diffusion のモデルセットアップ
url = "https://www.aozora.gr.jp/cards/000148/files/773_14560.html" # 青空文庫のURL指定、これは「こころ」
words = get_frequent_words(url)# 頻出語の抽出
image = generate_cover(pipe, words)# 画像の生成、確認
plt.imshow(image)
```
### 実行例
#### 人間失格
頻出単語は
`自分 事 無い 子 人間 女 人 顔 時 気 父 家 酒 者 気持ち 中 所 罪 ちゃん 道化 言葉 頃 実 矢張り 一人 所謂 男 部屋 絵 目`
となり、**なんだか強そうな女性**のイメージが生成されました。
抽象画という入力を加えているからか、しっかり絵画風の出力が得られています。

![人間失格.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/03cfb0bd-c63e-6638-efae-7031f8f539a0.png)

#### 風立ちぬ。
頻出単語は
`私達 中 自分 少し 目 方 日 気 上 病人 間 顔 手 前 父 雪 達 谷 風 殆ど 山 幸福 一人 サナトリウム 急 小さな 窓 いつも 月 もっと`
となり、どこか**暖かくもさみしさも感じる一枚**に仕上がっています。木の葉なのか空の雲なのか境界が曖昧な描写が個人的には結構好きな出力結果です。
![風立ちぬ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/806954d6-63d4-d1f6-7ddd-53d5cb3bbd30.png)

#### こころ
頻出単語は
`先生 事 奥さん 時 父 自分 人 方 気 母 前 御嬢 中 所 目 顔 上 言葉 間 二人 今 同じ 日 心 通り 妻 少し 口 後 人間`
案の定「先生」というフレーズが最も使われていました。そして出力結果には**なんとも言えない表情の二人**が出力されています。ここに写っているのは先生なのか奥さんなのか、はたまた他の登場人物とも思わせられる意味深な一枚です。
![こころ１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/d844b2c2-fb6f-fde2-7424-58621404a593.png)

#### 人間椅子
頻出単語は
`椅子 中 様 時 手紙 人 上 世界 不思議 人間 外 ホテル 間 奥様 方 男 御 肉体 非常 美しい 部屋 夫人 前 身体 余り 膝 若し 目 為 大きな`
**ご婦人が椅子の上に座っている**というシンプルな構図です。この椅子の中に、いえ、この先はやめておきましょう。
![人間椅子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233070/21a738df-2208-9c5b-edf3-8e1ad10b5f76.png)

# 結果

今回は青空文庫のテキストデータと最近流行りの画像生成AIモデルを使って、**「青空文庫の小説の表紙をAIに自動生成させる」** ことにチャレンジしてみました。

結果としてはかなりよさげで、もし**自動生成された表紙が一覧でならんでいたら青空文庫の本をジャケ読みすることも可能**になるのではないでしょうか。

一方、課題としては**出力される画像に余白のような空間が残ってしまう**点を感じました。text-to-imageモデルへのテキストデータの出力時には、ＳＮＳで「**呪文**」と呼ばれているように特定のキーワードを用いた画像の調整が必要になってきます。今回は「抽象画」というキーワードを用いてある程度結果を期待に近い形に工夫をしましたが、それ以外にも色々な工夫ができそうです。

気になった方はぜひ今回の記事を参考にご自分の手元で実践してみてください。

# 終わりに
こちらは[富士通クラウドテクノロジーズ株式会社 Advent calendar 2022](https://qiita.com/advent-calendar/2022/fjct) の10日目の記事でした。

明日は @ks2022 さんの「**チームで障害訓練した話**」です。
自分自身、現在はクラウドサービスの品質管理に関わっているので内容が気になるところですね。

また、FJCTの[Techブログ](https://tech.fjct.fujitsu.com/)で[エンジニアへのインタビュー記事](https://tech.fjct.fujitsu.com/entry/fjctqualityup-ansible)を執筆したりもしているのでよければそちらもご覧ください。

それでは、良い年末を。