はじめに

実用的な文章であれ,非実用的な文章―――たとえば二次創作小説,SS,怪文書,その他―であれ,よりよいものを書こうとするには,どうすればよいでしょうか.以前の記事では,その目的で,執筆の最中,あるいは執筆後の推敲に使える手法として,日本語変換システム『ATOK』およびそれに統合して利用できる各種辞書であるとか,『一太郎』に付属する校正ツールであるとか,あるいは今はやりの大規模言語モデル(LLM)だとかを簡単に紹介しました.LLM およびそれによって得られる生成物の利用については,日進月歩で変わりゆく分野であることから,いまだに議論が続いていますが,各種ガイドラインに従う限り,有用な技術であることは変わりありません.LLM を使えば,先の記事に示したように,自分の書いたものに対する定性的分析は,自然言語――われわれが普段使う言葉――を用いて,容易に行えることは否定できません.

ただ,これには取りこぼしがあります.最近では,自然言語処理(NLP)と言えば LLM といった風潮があります.ただ,実際のところはそうではなく,NLP というのは,LLM 以外も含んだ,より広い概念であるはずです.特に,データ傾向の可視化などに用いられる,テキストマイニングと呼ばれる手法は,文章の定量的解析に,いまだに有用な技術です.もちろん,LLM でもプロンプトを工夫すればできるのかもしれませんが,やや牛刀を以て鶏を割くというきらいがあります.とりわけ,非実用的な文章を解析しようとするときに,いちいち LLM を利用して,大量の計算資源を浪費するのは,持続可能な開発という点でも問題があると思います――というのは半分冗談ではありますが.

そこで,この記事では,テキストマイニングを中心とする伝統的な NLP の技法を用いて,自作の非実用的な文章を解析した試みを,備忘録としてまとめておきます.まずは,テキストを場面ごとに区切り,文体のダッシュボードとでも呼ぶべきものを作るべく,基礎的な指標を抽出して比較することにしました.

なお,先ほどのようにとうとう語ってはみましたが,実のところ私は NLP の専門家でも,ましては情報学の体系的教育を受けた人間でもないので,以下は――あるいはこれまでの話も――話半分で,自己責任のもとお読みください.以下の手順は,LLM にも示唆を求めつつ――惜しむらくは,このご時世,自分で一からコードを書くことの意義が揺らいでいることです――,自分で適宜修正を行いました.

準備

NLPのツールとしては,Python で書かれた有用なものが多く存在するので,それを利用できる環境を整えればよいと思います.私が利用している環境は,Windows 11 上の WSL2 (Ubuntu) です.これには新しめの Python 3 がついてくるはずです.また,テキストエディタは Visual Studio Code としました.これを構築する方法は,どこにでも転がっているし,いっそのこと LLM に訊けば分かることなので,ここでは示しません.

具体的には pandas ――これはデータ解析に汎用的に使われるツールですが――や spaCy を使うのですが,前者はともかく,後者は apt では入らず,pip などを用いる必要があります.ただ,グローバルで pip を普通に使ってしまうと,ライブラリが入り乱れて後々面倒になるので,仮想環境を使うことにしました.仮想環境といえば venv というイメージがありましたが,最近は uv というものがはやりなので,これを使います.

詳しくは公式のウェブサイトを見ていただきたいのですが,uv は以下で導入できます(sudo 不要):

1
curl -LsSf https://astral.sh/uv/install.sh | sh

仮想環境の構築は以下の通りです(ディレクトリ名は好きなものに変えてください):

1
2
3
mkdir nlp-playgrounds
cd nlp-playgrounds
uv init

そして,くだんの pandas, spaCy と,spaCy のモデル,ついでにビジュアライズ用に matplotlib と seabornを以下で導入します:

1
2
uv add pandas spacy matplotlib seaborn
uv run python -m spacy download ja_core_news_sm

また,ビジュアライズで使う日本語フォントも導入しておきます:

1
sudo apt install fonts-noto-cjk

道具立てがそろったので,解析するテキストを選定します.ここでは,以前 pixiv に投稿した二次創作小説 を利用します.もしネタバレを気にされるなら――そのような方は少ないかもしれませんが――,あらかじめこの小説を読んでおいてください.これでも私が書いた中では癖が少ない,まだましなものです.以下の記事では,説明の都合上,小説のあらすじにも触れますので,ご承知おきください.

以前書いたように,私は pixiv に直接書き付けるのではなく,Obsidian にて Markdown ファイルとして保存してから,適宜変換しています.よって,そのファイルを mihanada.md として上記の nlp-playgrounds ディレクトリに保存しておきました.ここではシーンごとに水平線---で区切っているので,後に示すように,それを利用することにします.ちなみに,各シーンの要約は下記の通りです.

  1. 駅~洋食店:二人が新幹線から駅に降り立ち,近くの百貨店にある洋食店で昼食をとる.
  2. 美術館:この地に生を受けたデザイナーの回顧展で,ヒロインがあるドレスに圧倒され,競走生活と重ね合わせる.
  3. 歴史博物館:打って変わって,県で古くから生活に用いられてきた布地の展示を見ていて,社会見学に来たとある子どもと交流することとなる.
  4. 夕暮れのベンチ:城近くで,未来に向けた会話をする.

解析

とりあえず,ここでは以下の項目を解析することにしましょう:

  1. 文字数・文数・平均文長:感情が動く場面や情景描写が深い場面では一文が長くなる傾向がある
  2. 会話文比率:全文字数のうち,「」で囲まれた文字数が占める割合
  3. 形容詞・動詞の比率:情景描写(色や温度)が多い場面は形容詞が,動作が多い場面は動詞が増える

そのためには,以下のようなソースコードを実行します.昔はこういうものも手で書いていましたが,今は LLM がまたたく間に書いてしまいますね.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import re
import pandas as pd
import spacy
import matplotlib.pyplot as plt
import seaborn as sns


# spaCyの日本語モデルをロード(事前に python -m spacy download ja_core_news_sm を実行)
nlp = spacy.load("ja_core_news_sm")


def analyze_novel(text):
    # 水平線(---)でテキストを場面ごとに分割
    scenes = re.split(r'\n---\n', text)
    
    results = []
    
    for i, scene_text in enumerate(scenes):
        scene_text = scene_text.strip()
        if not scene_text:
            continue
            
        # 1. 基本的な文字数のカウント(空白を除く)
        clean_text = re.sub(r'\s+', '', scene_text)
        total_chars = len(clean_text)
        
        # 2. 会話文の抽出と比率計算
        # 「」で囲まれた部分を抽出
        dialogues = re.findall(r'「(.*?)」', scene_text)
        dialogue_chars = sum(len(d) for d in dialogues)
        dialogue_ratio = (dialogue_chars / total_chars) * 100 if total_chars > 0 else 0
        
        # 3. 形態素解析(spaCy)による文数と品詞のカウント
        doc = nlp(scene_text)
        
        # 文の数(spaCyの文境界判定を使用)
        sentences = list(doc.sents)
        num_sentences = len(sentences)
        avg_sentence_length = total_chars / num_sentences if num_sentences > 0 else 0
        
        # 品詞のカウント
        pos_counts = {'名詞': 0, '動詞': 0, '形容詞': 0, '副詞': 0}
        for token in doc:
            if token.pos_ in ['NOUN', 'PROPN']:
                pos_counts['名詞'] += 1
            elif token.pos_ == 'VERB':
                pos_counts['動詞'] += 1
            elif token.pos_ == 'ADJ':
                pos_counts['形容詞'] += 1
            elif token.pos_ == 'ADV':
                pos_counts['副詞'] += 1
                
        # 結果を辞書に格納
        results.append({
            '場面': f'シーン {i+1}',
            '総文字数': total_chars,
            '文数': num_sentences,
            '平均文長': round(avg_sentence_length, 1),
            '会話率(%)': round(dialogue_ratio, 1),
            '名詞数': pos_counts['名詞'],
            '動詞数': pos_counts['動詞'],
            '形容詞数': pos_counts['形容詞']
        })
        
    # pandasのDataFrameにして見やすくする
    df = pd.DataFrame(results)
    return df


# 分析の実行
def main():
    with open('mihanada.md', 'r', encoding='utf-8') as f:
        novel_text = f.read()
    df_results = analyze_novel(novel_text)
    print(df_results)
    try:
        plt.rcParams['font.family'] = 'sans-serif'
        plt.rcParams['font.sans-serif'] = ['Noto Sans CJK JP', 'sans-serif']
    except:
        pass

    # 品詞の密度(100文字あたりの出現回数)を計算してデータフレームに追加
    df_results['名詞密度'] = (df_results['名詞数'] / df_results['総文字数']) * 100
    df_results['動詞密度'] = (df_results['動詞数'] / df_results['総文字数']) * 100
    df_results['形容詞密度'] = (df_results['形容詞数'] / df_results['総文字数']) * 100

    # グラフの描画設定(2行2列のレイアウト)
    fig, axes = plt.subplots(2, 2, figsize=(12, 8))
    fig.suptitle('小説「水縹の夏、縫い合わされた軌跡」場面ごとの文体推移', fontsize=16)

    # 1. 会話率の推移
    sns.lineplot(ax=axes[0, 0], data=df_results, x='場面', y='会話率(%)', marker='o', color='blue')
    axes[0, 0].set_title('会話率の推移')
    axes[0, 0].set_ylim(0, 40)

    # 2. 平均文長の推移
    sns.lineplot(ax=axes[0, 1], data=df_results, x='場面', y='平均文長', marker='s', color='green')
    axes[0, 1].set_title('平均文長の推移')

    # 3. 品詞密度の推移(名詞・動詞・形容詞)
    sns.lineplot(ax=axes[1, 0], data=df_results, x='場面', y='名詞密度', marker='^', label='名詞')
    sns.lineplot(ax=axes[1, 0], data=df_results, x='場面', y='動詞密度', marker='v', label='動詞')
    sns.lineplot(ax=axes[1, 0], data=df_results, x='場面', y='形容詞密度', marker='d', label='形容詞')
    axes[1, 0].set_title('品詞密度(100文字あたり)の推移')
    axes[1, 0].set_ylabel('密度')

    # 4. 文字数と文数の関係(棒グラフ)
    df_results[['場面', '総文字数']].set_index('場面').plot(kind='bar', ax=axes[1, 1], color='skyblue')
    axes[1, 1].set_title('場面ごとの総文字数')
    axes[1, 1].tick_params(axis='x', rotation=0)

    plt.tight_layout()
    # グラフを画像として保存(WSL2環境で確認しやすくするため)
    plt.savefig('novel_analysis_dashboard.png', dpi=300)
    print("グラフを 'novel_analysis_dashboard.png' として保存しました。")


if __name__ == "__main__":
    main()

これを実行すると,コンソールに表が出力されるとともに,それを可視化した画像が出力されます.表の出力を整形したものを示します:

場面 総文字数 文数 平均文長 会話率(%) 名詞数 動詞数 形容詞数
シーン 1 1575 52 30.3 14.7 238 135 36
シーン 2 1241 42 29.5 7.3 196 108 23
シーン 3 1240 38 32.6 8.9 185 121 32
シーン 4 864 31 27.9 30.8 108 81 17

そして,画像を以下に示します:

図:会話率,平均文長,品詞密度,総文字数の推移.

特筆すべきは,以下の点でしょうか.

  • シーン 4(夕暮れのベンチ)の特異性

    • シーン 1〜3 まで 15% 未満に抑えられていた会話率が,シーン 4 で一気に跳ね上がっている.平均文長も最短である.
    • シーン 1〜3 までは情景描写や内省を地の文で積み重ねてきたが,最後のシーン 4 では,二人の対話によって一気に解放されるという構造をもつことがうかがえる.
  • シーン 2(美術館)の「静」とシーン 3(歴史博物館)の「動」

    • シーン 2 では,名詞の密度が高く,会話が少ない.「ドレス」「リボン」「縫い目」といったモノ(名詞)に対する観察と,主人公の内面的な回想に焦点が当たっているためであると考えられる.
    • シーン 3 では,動詞と形容詞の密度が最大で,平均文長が最長である.社会見学の列が通り過ぎる,帽子を直してあげる,少女が駆けていくといった「動き(動詞)」と,布の質感や少女の表情などの描写が多いためか.平均文長が最も長いのは,一文の中に複数の動作や情景を詰め込んでいるため.

参考書籍

Python や NLP が初めての方なら,下記が手に取りやすいと思います.身近な例を通じて,おもしろおかしく基礎を学べる本です.ただ通読するだけでも楽しめるという,異色の技術書です.ただ,体系的に学ぶというよりも,あくまでも紹介あるいはダイジェストというきらいがあります.

もう少し体系的に伝統的な NLP の技法を学ぶなら,たとえば下記の本がいいのかもしれません.たまたま書店で見かけて,手に取りました.いろいろな技法がぎゅっと詰まっていて,個人的には辞書的にも使えそうな気はしました.コード例も豊富です.ただ,人によってはとっつきにくいと思われるかもしれません.もしおすすめの書籍等があれば,ご紹介いただけますと幸いです.