データセットを加工する作業は持っているデータによって作業内容が大きく変わるため、どのような手順で作業をするかがケースバイケースなことが多いかと思います。本記事はデータセットの作成、加工の一例として、ラベルのない日本語のデータセットにラベルを貼る一連の作業について述べます。

 

はじめに

昨年自然言語処理学会へ初めて行ってきました。ずっと数値データと親しんでいた私は大規模言語モデル(LLM)の隆盛をひしひしバチバチと感じられてとても楽しかったです。なかでも印象に残った研究発表のひとつがこちらの研究です。LLMの根幹であるTransformerの各アテンションヘッドがどのような単語群に注目しているかを分析した研究ですが、「これ日本語のモデルでやったらどうなるのかな」と思ったので、自然言語処理と親しむ入口としてちょっと試してみます。そのためには①日本語で事前学習されたTransformerベースのモデルと②日本語の自然言語処理タスク向けのデータセットが必要です。

①については東北大学が公開している日本語BERTモデルがあるので、こちらを使うことができそうです。問題は②で、ネットを漁っても自分のモチベを上げてくれるような良い感じのデータセットがなかなかでてきません。そこで本記事では、公開されている青空文庫のデータと日本語Wikipedia上の情報を組み合わせて分類問題用のデータセットを作成してみます。

環境: Google Colaboratory

 

青空文庫のデータの読み込み

青空文庫は著作権の切れた(または、残っているが公開が許可されている)作品を有志が入力、校正を行ってインターネット上で公開してくれているサイトです。ありがたいことにテキストやHTMLファイルがgithubで公開されていて、取り扱い基準の範囲内で自由に利用することができます。今回は公開されているテキストファイルを言語モデルに入力しやすい形に変換(例えばルビを外すなど)した上で再配布されたこちらのデータセットを利用します。

import pandas as pd
df = pd.read_json("hf://datasets/globis-university/aozorabunko-clean/aozorabunko-dedupe-clean.jsonl.gz", lines=True)

こちらを実行すると、3つカラムを持つpandas.DataFrameが得られます。textは本文、footnoteは作品ページの末尾に記載された作品情報です。metaは青空文庫の各作品ページが持っているIDや著者などの属性で、ここでは辞書型で読み込まれます。

df[:3]
text footnote meta
0 深いおどろきにうたれて、… 底本:「スケッチ・ブック」新潮文庫、新潮社   1957(昭和32)年5月20日発行… {‘作品ID’: ‘059898’, ‘作品名’: ‘ウェストミンスター寺院’, ’作品名読…
1 いざ、これより樂しまむ、、、、… 底本:「スケッチ・ブック」岩波文庫、岩波書店   1935(昭和10)年9月15日第1刷… {‘作品ID’: ‘056078’, ‘作品名’: ‘駅伝馬車’, ‘作品名読み’: ’えき…
2 すべてよし。。。。… 底本:「スケッチ・ブック」新潮文庫、新潮社   1957(昭和32)年5月20日発行… {‘作品ID’: ‘060224’, ‘作品名’: ‘駅馬車’, ‘作品名読み’: ’えきば…
df.loc[0, "meta"]
{'作品ID': '059898',
 '作品名': 'ウェストミンスター寺院',
 '作品名読み': 'ウェストミンスターじいん',
 'ソート用読み': 'うえすとみんすたあしいん',
 '副題': '',
 '副題読み': '',
 '原題': '',
 '初出': '',
 '分類番号': 'NDC 933',
 '文字遣い種別': '新字新仮名',
 '作品著作権フラグ': 'なし',
 '公開日': '2020-04-03',
 '最終更新日': '2020-03-28',
 '図書カードURL': 'https://www.aozora.gr.jp/cards/001257/card59898.html',
 '人物ID': '001257',
 '姓': 'アーヴィング',
 '名': 'ワシントン',
 '姓読み': 'アーヴィング',
 '名読み': 'ワシントン',
 '姓読みソート用': 'ああういんく',
 '名読みソート用': 'わしんとん',
 '姓ローマ字': 'Irving',
 '名ローマ字': 'Washington',
 '役割フラグ': '著者',
 '生年月日': '1783-04-03',
 '没年月日': '1859-11-28',
 '人物著作権フラグ': 'なし',
 '底本名1': 'スケッチ・ブック',
 '底本出版社名1': '新潮文庫、新潮社',
 '底本初版発行年1': '1957(昭和32)年5月20日',
 '入力に使用した版1': '2000(平成12)年2月20日33刷改版',
 '校正に使用した版1': '2000(平成12)年2月20日33刷改版',
 '底本の親本名1': '',
 '底本の親本出版社名1': '',
 '底本の親本初版発行年1': '',
 '底本名2': '',
 '底本出版社名2': '',
 '底本初版発行年2': '',
 '入力に使用した版2': '',
 '校正に使用した版2': '',
 '底本の親本名2': '',
 '底本の親本出版社名2': '',
 '底本の親本初版発行年2': '',
 '入力者': 'えにしだ',
 '校正者': '砂場清隆',
 'テキストファイルURL': 'https://www.aozora.gr.jp/cards/001257/files/59898_ruby_70679.zip',
 'テキストファイル最終更新日': '2020-03-28',
 'テキストファイル符号化方式': 'ShiftJIS',
 'テキストファイル文字集合': 'JIS X 0208',
 'テキストファイル修正回数': '0',
 'XHTML/HTMLファイルURL': 'https://www.aozora.gr.jp/cards/001257/files/59898_70731.html',
 'XHTML/HTMLファイル最終更新日': '2020-03-28',
 'XHTML/HTMLファイル符号化方式': 'ShiftJIS',
 'XHTML/HTMLファイル文字集合': 'JIS X 0208',
 'XHTML/HTMLファイル修正回数': '0'}

このままだと使いにくいのでmeta列を複数の列へ展開します。辞書を要素とするカラムを複数カラムへ展開する方法をChatGPTに聞いたところ、applyメソッドにpd.Seriesを渡せばいけるみたいです。展開してできた55列のDataFrameをもとのDataFrameに結合します。

# metaをひとつひとつのカラムに展開する
df = df.join(df['meta'].apply(pd.Series))

出版年を取得する

底本(青空文庫の各ページの元になった本)の出版年の情報が「底本初版発行年1」というカラムで与えられているので、これをdatetime型に変換しておきます。(実際に解析に使うかどうかはわかりませんが、日時データはdatetimeに変換しておきたくなってしまいます。)ざっと確認をすると、「西暦(元号)年〇月×日」の形式で書かれているようです。正規表現を使って西暦年、月、日を取り出してdatetime型に変換します。それ以外の形式のものや、空白の作品もありますが、数も多くないので今回はそれらは無視してしまいます。

df['底本初版発行年1'].sort_values()
底本初版発行年1
4870
1153
3806
3484
3174
6621 2019(令和元)年8月5日
6977 2021(令和3)年7月21日
1522
1514 1988(昭和63)年6月30日
10758 昭和11(1936)年1月1日

ChatGPTに正規表現パターンを作ってもらいました。正規表現のパターンを行き当たりばったりで作っていつも四苦八苦しているので、非常に助かります。文字列の「西暦(元号)年〇月×日」から年月日それぞれの数字を抽出して、ハイフンを間に挟んだ「西暦-〇-×」の形式の文字列を生成します。作成した文字列の日付をpd.to_datetime()でdatetime型に変換したら完成です。

import numpy as np
# 「西暦(元号)年〇月×日」から「西暦-〇-×」への変換
# 正規表現パターンのマッチに失敗していたら(nullが返ってきていたら)その作品のdate列はnp.nanにする
df['date'] = df['底本初版発行年1'].str.extract(r'([0-9]+?)(.{2,3}[0-9]*)年([0-9]{1,2})月([0-9]{1,2})日').apply(
    lambda x: "-".join(x) if x[0]==x[0] else np.nan, axis=1)
df["date"] = pd.to_datetime(df["date"], errors='coerce')
df[["底本初版発行年1", "date"]]
底本初版発行年1 date
0 1957(昭和32)年5月20日 1957-05-20
1 1935(昭和10)年9月15日 1935-09-15
2 1957(昭和32)年5月20日 1957-05-20
3 1957(昭和32)年5月20日 1957-05-20
4 1957(昭和32)年5月20日 1957-05-20
16946 1978(昭和53)年6月16日 1978-06-16
16947 1995(平成7)年9月18日 1995-09-18
16948 2007(平成19)年4月10日 2007-04-10
16949 1995(平成7)年9月18日 1995-09-18
16950 1953(昭和28)年11月25日 1953-11-25

文字遣い種別

ここまでの処理の結果得られたDataFrameを見てみます。試しに主要なカラムを出力してみると、以下のようになります。

# 作品IDをindexに設定する
df.set_index("作品ID", drop=True, inplace=True)
# 本記事の筆者は坂口安吾が好きです
df.loc[(df.姓=="坂口")&(df.名=="安吾"),
 ['footnote', '作品名', 'date', '人物ID', '姓', '名', '姓読み', '名読み', "文字遣い種別"]][:10]
作品ID footnote 作品名 date 人物ID 姓読み 名読み 文字遣い種別
056797 底本:「風と光と二十の私と・いずこへ 他十六篇」岩波書店、岩波文庫   2008(平成2… 青い絨毯 2008-11-14 001095 坂口 安吾 さかぐち あんご 新字新仮名
042877 底本:「坂口安吾全集 05」筑摩書房   1998(平成10)年6月20日初版第1刷発行… 青鬼の褌を洗う女 1998-06-20 001095 坂口 安吾 さかぐち あんご 新字新仮名
045884 底本:「坂口安吾全集 03」筑摩書房   1999(平成11)年3月20日初版第1刷発行… 諦らめアネゴ 1999-03-20 001095 坂口 安吾 さかぐち あんご 新字旧仮名
045733 底本:「坂口安吾選集 第十巻エッセイ1」講談社   1982(昭和57)年8月12日第1… 諦めている子供たち 1982-08-12 001095 坂口 安吾 さかぐち あんご 新字新仮名
056808 底本:「堕落論・日本文化私観 他二十二篇」岩波文庫、岩波書店   2008(平成20)年… 悪妻論 2008-09-17 001095 坂口 安吾 さかぐち あんご 新字新仮名
042868 底本:「坂口安吾全集 05」筑摩書房   1998(平成10)年6月20日初版第1刷発行… 悪妻論 1998-06-20 001095 坂口 安吾 さかぐち あんご 新字旧仮名
042902 底本:「坂口安吾全集 04」筑摩書房   1998(平成10)年5月22日初版第1刷発行… 足のない男と首のない男 1998-05-22 001095 坂口 安吾 さかぐち あんご 新字新仮名
045935 底本:「坂口安吾全集 13」筑摩書房   1999(平成11)年2月20日初版第1刷発行… 明日は天気になれ 1999-02-20 001095 坂口 安吾 さかぐち あんご 新字新仮名
045809 底本:「坂口安吾全集 01」筑摩書房   1999(平成11)年5月20日初版第1刷発行… 新らしき性格感情 1999-05-20 001095 坂口 安吾 さかぐち あんご 新字旧仮名
045734 底本:「坂口安吾選集 第十巻エッセイ1」講談社   1982(昭和57)年8月12日第1… 新らしき性格感情 1982-08-12 001095 坂口 安吾 さかぐち あんご 新字新仮名

各作品は「文字遣い種別」という情報を持っています。これはその作品で使われている漢字と仮名遣いの情報で、新字/旧字、新仮名/旧仮名それぞれの組合せの合計4パターン(とその他)があります。本記事では事前学習済みの言語モデル用にデータセットを作成しているため、学習データで使われている文書と揃えるために新字新仮名の作品のみを使うことにします。

# 文字遣い種別の一覧
df["文字遣い種別"].value_counts()
文字遣い種別 count
新字新仮名 10246
新字旧仮名 4515
旧字旧仮名 2143
旧字新仮名 32
その他 15
# 新字新仮名の作品のみを残す
df = df.loc[df.文字遣い種別=="新字新仮名", :]

さて、本記事は分類問題用のデータセットを作る記事です。簡単に思いつく問題設定として「本文またはその一部からそれを書いた著者を当てる」という問題が挙げられますが、本データセットに含まれる著者(人物ID)は膨大で、これを分類問題にするのは難しそうです。

# DataFrameに含まれる著者の人数
df["人物ID"].nunique()
655

そこで、いくつかの著者をグルーピングすることを考えます。ここではそれぞれの著者が属する「文芸思潮」を使うことにします。

 

wikipediaのページから著者の文芸思潮の情報を取得する

ある特定の時代に同様の思想を持って活動していた作家たちをさして「○○派」と呼ぶことがあります。(例えば坂口安吾は無頼派とか新戯作派とか呼ばれます。)これを文芸思潮といいます。同じ時代にあり同じ思想を持っていた作家たちの作品に共通する特徴を、言語モデルで抽出することができたら面白そうです。青空文庫のメタデータの中に文芸思潮の情報はないので、日本語Wikipediaから取ってくることにします。wikipediaパッケージを使って各著者のページのhtmlソースを取得し、そこから文芸思潮の情報を抽出します。

! pip install wikipedia
import wikipedia

検索対象記事の言語を日本語に設定します。

wikipedia.set_lang("ja")

ページの検索にはsearchメソッドを使います。試しに「坂口安吾」を引数にして実行してみると、本人のページの他に著作等についてのページのタイトルが返ってくるのがわかります。本記事では著者名とタイトルが一致するページから文芸思潮の情報を抽出することにします。

wikipedia.search("坂口安吾")
['坂口安吾',
 '桜の森の満開の下',
 '白痴 (坂口安吾)',
 '織田信長 天下を取ったバカ',
 'ちくま文庫',
 '真珠 (坂口安吾)',
 '不連続殺人事件',
 '明治開化 安吾捕物帖',
 '石川淳',
 '小説新潮']

Wikipediaの各著者のページを取得する

まずは青空文庫の各ページのメタデータから著者名のカラムを作成します。姓と名をそのまま結合したものを著者名(author)とします。青空文庫のデータセットの中には海外著者の作品の翻訳版も含まれているため、すべての作品で「姓+名」を著者名にするのは乱暴ですが、それによって生じる問題はここでは無視します。

df["author"] = df[["姓", "名"]].apply(lambda x: x[0]+x[1], axis=1)

著者名でページを検索し、著者名と一致するページがあった場合はwikipedia.page()を使ってそのページの情報を取得します。

pages = []
for author in df["author"].unique():
    # 著者名でページ検索
    li_title = wikipedia.search(author)
    # 著者名とタイトルが一致するページがあれば、ページ情報を取得する
    if len(li_title) > 0 and author in li_title:
      try:
        pages.append(wikipedia.page(li_title[li_title.index(author)]))
      except:
        pass

2/3くらいの著者のページが取得できたようです。

print("著者総数: ", df["author"].nunique())
print("取得できたページ情報総数: ", len(pages))
著者総数:  655
取得できたページ情報総数:  460

wikipediaのページを確認してみると、文芸思潮はページ上部右側の基本情報があるところに「文学活動」という項目で以下のように記載されています。該当箇所のhtmlは以下のようになっており、「文学活動」の直下のtdタグ内のaタグの中身を抽出できれば良さそうです。

<tr class="" style="" itemprop="">
  <th scope="row" style="text-align:left; white-space:nowrap;" >文学活動</th>
  <td class="" style="" itemprop="">
    <a href= ページのリンク title="無頼派">無頼派</a>、
    <a href=ページのリンク class="mw-redirect" title="新戯作派">新戯作派</a>
  </td>
</tr>

htmlソースから文芸思潮を抽出する

htmlソースのどの部分に文芸思潮があるかわかったので、beautifulsoup4を使って取り出します。文字列型のfind()メソッドで「文学活動」がhtmlソースの中にあるか検索し、どこかに存在する(0以上の整数が返ってくる)場合はそれ以降のソースをbeautifulsoupで解析します。ここではfind()メソッドで「文芸活動」の直下のtdタグを検索し、さらにその中のaタグをfind_all()メソッドですべて取り出しています。

from bs4 import BeautifulSoup
target = "文学活動"
dict_author_group = {}
for page in pages:
  # 「文学活動」の項目が存在するか判定する
  # find()は与えた文字列の開始位置 or 存在しない場合は-1を返す
  start_index = page.html().find(target)
  if start_index>-1:
    # 「文学活動」以降のhtmlを解析
    soup = BeautifulSoup(page.html()[start_index:], 'html.parser')
    if len(soup.find_all("td"))>0:
      # 最初に見つかったtdタグの中のaタグが文芸思潮
      dict_author_group[page.title] = [x.get("title") for x in soup.find("td").find_all("a")]
      print(page.title, dict_author_group[page.title])
有島武郎 ['白樺派']
石川啄木 ['自然主義文学']
泉鏡花 ['ロマン主義', '幻想文学', '観念小説']
伊藤永之介 ['労農派']
伊藤左千夫 ['アララギ派', 'ロマン主義']
岩野泡鳴 ['自然主義文学']
内田魯庵 ['社会小説']
梅崎春生 ['第一次戦後派']
大下宇陀児 []
岡本かの子 [None, None, None, None]
小栗風葉 ['硯友社']
尾崎紅葉 ['擬古典主義', '硯友社']
織田作之助 ['無頼派', '新戯作派']
葛西善蔵 []
梶井基次郎 ['新興芸術派']
片岡鉄兵 ['プロレタリア文学', '新感覚派']
加藤道夫 ['文学座', '雲の会']
河東碧梧桐 []
菊池寛 ['新現実主義', '新思潮']
金史良 []
国木田独歩 ['ロマン主義', '自然主義文学']
久保田万太郎 ['江戸', '歌舞伎', '新派']
久米正雄 ['新思潮']
黒岩涙香 ['時事問題', '評論']
幸田露伴 ['擬古典主義', '写実主義']
小林多喜二 ['プロレタリア文学', '戦旗']
斎藤茂吉 ['アララギ派']
坂口安吾 ['無頼派', '新戯作派']
佐々木邦 []
佐藤春夫 ['主知主義', '耽美派', '「芸術詩派」 (存在しないページ)']
島木健作 ['転向']
島木赤彦 ['アララギ']
島崎藤村 ['ロマン主義', '自然主義文学']
島村抱月 ['自然主義文学', '新劇']
相馬泰三 []
高浜虚子 ['ホトトギス (雑誌)']
高見順 ['無頼派']
太宰治 ['無頼派', None, '新戯作派']
立原道造 ['「四季派」 (存在しないページ)']
谷崎潤一郎 ['耽美派', '悪魔主義', '古典']
田山花袋 ['自然主義文学']
坪内逍遥 ['写実主義']
十返肇 []
徳田秋声 ['自然主義文学']
徳富蘇峰 ['時事問題', '評論', '伝記']
徳冨蘆花 ['ロマン主義']
徳永直 ['プロレタリア文学', '戦旗']
内藤鳴雪 ['ホトトギス (雑誌)']
中勘助 [None]
永井荷風 ['耽美派']
中島敦 ['斗南先生']
長塚節 ['アララギ']
中野鈴子 ['プロレタリア文学']
夏目漱石 ['余裕派', '反自然主義文学']
野村胡堂 ['日本作家クラブ']
萩原朔太郎 ['象徴主義', '「芸術詩派」 (存在しないページ)', 'アフォリズム', '口語自由詩', '神秘主義']
長谷川伸 [None, None, None, None]
葉山嘉樹 ['プロレタリア文学']
広津柳浪 ['悲惨小説']
二葉亭四迷 ['写実主義', '言文一致']
堀辰雄 ['王朝時代']
正宗白鳥 ['自然主義文学']
松崎天民 []
松本泰 ['三田文学', '犯罪']
宮沢賢治 ['理想主義', None]
宮本百合子 []
三好十郎 ['プロレタリア文学', '無頼派']
室生犀星 ['理想主義']
八木重吉 [None, None, None, None]
柳宗悦 ['白樺派', '民藝運動']
山川方夫 ['三田文学']
山田美妙 ['写実主義', '硯友社']
横光利一 ['新感覚派']
与謝野晶子 ['ロマン主義']
吉井勇 ['耽美派']
若松賤子 ['女学雑誌']
若山牧水 ['自然主義文学']

得られた辞書から、データセットのDataFrameに文芸思潮のリストのカラムを追加します。

df["li_group"] = df["author"].map(dict_author_group)
df.shape
(10246, 60)

このままだと一人の著者に複数の文芸思潮が割り当てられていることがあり、単純な分類問題にするのが難しいため、ひとつの文芸思潮に絞った列も用意したいです。ここでは簡単に処理するために(また分類問題が難しくなるのを避けるために)、割り当てられた文芸思潮の中で、データセット内に含まれる同じ文芸思潮を割り当てられた著者が一番多いものを選ぶことにします。

# key: 文芸思潮 value: 著者名 とした辞書を作成する
# 文芸思潮のリストを作成
list_group = []
for v in dict_author_group.values():
  list_group.extend(v)
# key: 文芸思潮 value: 著者名 とした辞書
dict_group_author = {}
for group in pd.Series(list_group).unique():
  if group is not None:
    # groupを割り当てられた著者のリストを作成
    li = [k for k, v in dict_author_group.items() if group in v]
    dict_group_author[group] = li
# DataFrameのli_group列に適用するメソッド
def get_group(x):
  """
  parameters
  -----  
  x: list[str] 文芸思潮のリスト
  returns
  -----
  xの中で、割り当てられた著者が最大の文芸思潮(文字列)
  """
  if type(x) is list:
    # key: 各文芸思潮 value: 割り当てられた著者の人数 の辞書を作成して、人数で降順に並び替える
    temp = sorted({group:len(dict_group_author[group]) for group in x if group is not None}.items(),
                  key=lambda y: y[1],
                  reverse=True)
    # 並び替えた最初の要素(=割り当てられた著者が最大の文芸思潮)を返す
    if len(temp) > 0:
      return temp[0][0]
    return np.nan
df["group"] = df["li_group"].map(get_group)

それぞれの著者にひとつの文芸思潮を割り当てた結果、それぞれの文芸思潮に含まれる作品数は以下のようになりました。とりあえずなにがしかの分類モデルが学習できそうな件数が得られました。

df["group"].value_counts()
group count
無頼派 581
日本作家クラブ 260
ロマン主義 177
理想主義 133
耽美派 130
自然主義文学 107
余裕派 84
プロレタリア文学 83
新思潮 77
白樺派 60
三田文学 37
写実主義 36
新感覚派 36
王朝時代 28
新興芸術派 26
斗南先生 25
社会小説 20
第一次戦後派 16
ホトトギス (雑誌) 12
アララギ派 12
「芸術詩派」 (存在しないページ) 11
時事問題 6
江戸 5
硯友社 4
転向 3
アララギ 2
悲惨小説 2
「四季派」 (存在しないページ) 1
文学座 1
労農派 1
女学雑誌 1

また現段階でのデータセットの様子は以下のようになります。最後に今まで手付かずだった本文の処理を行っていきます。

df.loc[(~df.group.isnull()), ["作品名", "text", "author", "date", "li_group", "group"]].head()
作品ID 作品名 text author date li_group group
000201 或る女 一 新橋を渡る時、発車を知らせる二番目の鈴が、霧とまではいえない九月の朝の、煙った空… 有島武郎 1950-05-05 [白樺派] 白樺派
000202 或る女 二二 どこかから菊の香がかすかに通って来たように思って葉子は快い眠りから目をさました… 有島武郎 1950-09-05 [白樺派] 白樺派
001111 生まれいずる悩み 一 私は自分の仕事を神聖なものにしようとしていた。ねじ曲がろうとする自分の心をひっぱ… 有島武郎 1940-03-26 [白樺派] 白樺派
001144 惜みなく愛は奪う Sometimes with one I love, I fill myself with … 有島武郎 1955-01-25 [白樺派] 白樺派
000215 溺れかけた兄妹 土用波という高い波が風もないのに海岸に打寄せる頃になると、海水浴に来ている都の人たちも段々… 有島武郎 1988-12-16 [白樺派] 白樺派

 

本文を前処理する

最後に言語モデルに入力できる形に本文を整形していきます。上で見た通りこのデータセットには本文が英語のものも含まれますが、日本語の本文のみを対象にします。ここでは主に2つの処理を行います。

  1. 空白や記号の除去
  2. 段落による本文の分割

空白や記号の除去

文章の意味と直接関係のない空白や記号を削除します。こちらこちらを参考に、reパッケージを使って半角と全角の空白、記号を本文から取り除きます。

段落による本文の分割

言語モデルにひとつのサンプルとして投入できる文書の長さには制限があります。本記事ではとりあえず段落で区切ることでひとつの本文を複数のサンプルに分割します。段落の区切りである改行+全角スペース\n\u3000か、それに該当しない場合は改行\nを区切り文字として本文を分割します。

import re

def get_paragraphs(text):
  """
  本文全体を段落ごとに分割する
  段落の区切りが特殊だった場合はうまく分かれないことがあるので注意
  parameters
  -----
  text : str ある作品の本文全体
  returns
  -----
  list_para : List[str] 本文を分割した段落のリスト
  """
  # 段落に関連しない記号の削除
  # 半角、全角記号の削除
  text = re.sub(r'[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%…]', '', text)
  # 三点リーダーの削除
  text = re.sub(r"\u2026", "", text)
  # 段落ごとの文字列に分ける
  # 改行+全角スペース(\n\u3000)があれば、それを使って段落ごとに分ける
  if "\n\u3000" in text:
    paras = text.split("\n\u3000")
  # それ以外は改行で分ける  else:
    paras = text.split("\n")
  list_para = []
  for num, para in enumerate(paras):
    # 「改行+全角スペース」以外の全角記号の削除
    para = re.sub("[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\u3000-\u303F]", '', para)
    # 改行の削除
    para = re.sub(r"\n", "", para)
    list_para.append(para)
  return list_para

本文を段落へ分割すると以下のようになります。段落の区切りが特殊な作品はうまく段落に分かれない場合がありますが、本記事ではそれらは一旦無視します。

text = df.loc[df.index[0], "text"]
print("本文文字数: ", len(text))
print("本文冒頭:\n", text[:100])
print("本文末尾:\n", text[-100:])
print("\n")
for num, para in enumerate(get_paragraphs(text)):
  print("第{}段落 (文字数 {}): {} ~中略~ {}".format(num+1, len(para), para[:20], para[-20:]))
本文文字数:  10639
本文冒頭:
 深いおどろきにうたれて、
名高いウェストミンスターに
真鍮や石の記念碑となって
すべての王侯貴族が集まっているのをみれば、
今はさげすみも、ほこりも、見栄もない。
善にかえった貴人の姿、
華美と俗世の
本文末尾:
 たりに垂れて咲きみだれるのだ。こうして、人はこの世を去り、その名は記録からも記憶からも滅びるのだ。その生涯ははかない物語のようであり、その記念碑さえも廃墟となるのである。

原註 トマス・ブラウン卿。

第1段落 (文字数 260): 深いおどろきにうたれて名高いウェストミン ~中略~ ―クリストレロの諷刺詩一五九八年T・B作
第2段落 (文字数 174): 秋も更けて暁闇がすぐに黄昏となり暮れてゆ ~中略~ の闇のなかに身を没してゆくような気がした
第3段落 (文字数 170): わたしはウェストミンスター・スクールの中 ~中略~ 墓地からぬけ出してきた幽霊のように見えた
第4段落 (文字数 305): こういう陰鬱な僧院の跡を通って寺院に近づ ~中略~ そそりまた心を楽しくさせるものがあるのだ
第5段落 (文字数 122): 太陽は廻廊の中庭に黄色い秋の光を注ぎ中央 ~中略~ 輝いて蒼天に屹立しているのが眼にうつった
第6段落 (文字数 623): わたしは廻廊を歩いてゆきながらこの栄光と ~中略~ を墓へと押し流してゆくのを告げているのだ
第7段落 (文字数 310): わたしは足を進めて寺院の内部に通ずるアー ~中略~ した静けさをいやがうえにも強く感じるのだ
第8段落 (文字数 380): この場にみなぎっている荘厳さは魂を圧倒し ~中略~ えるのはほんの短い数年のあいだだけなのだ
第9段落 (文字数 701): わたしは詩人の墓所でしばらく時をすごした ~中略~ の輝かしい宝石言葉の金鉱脈を残したからだ
第10段落 (文字数 380): 詩人の墓所からわたしは歩みをつづけて寺院 ~中略~ る町のなかの邸を歩いているような気がする
第11段落 (文字数 851): わたしは立ちどまって一つの墓をしみじみと ~中略~ ての自覚をあらわす碑銘をわたしは知らない
第12段落 (文字数 489): 詩人の墓所と反対側の袖廊に一つの記念碑が ~中略~ 驚愕の場所ではなく悲哀と瞑想の場所である
第13段落 (文字数 200): こういう暗い円天井やしんとした側廊を歩き ~中略~ のをきくのはふしぎな感じがするものである
第14段落 (文字数 258): こういうふうにしてわたしは墓から墓へ礼拝 ~中略~ 足などふみこませまいとしているようだった
第15段落 (文字数 415): 中に入ると建築の華麗と精細な彫刻の美とに ~中略~ やな細工をした真鍮の手摺りでかこんである
第16段落 (文字数 579): この壮大さにはもの悲しいさびしさがあった ~中略~ た陰鬱な記念碑にむくいられようとしたのだ
第17段落 (文字数 247): この礼拝堂の両側にある小さな二つの側廊は ~中略~ 同情の溜め息の音をひびきかえしているのだ
第18段落 (文字数 240): メアリーが埋葬されている側廊には異様な憂 ~中略~ メアリーの数奇で悲惨な物語が渦巻いていた
第19段落 (文字数 234): ときどき聞えていた足音はこの寺院から絶え ~中略~ らあるのは忘却と塵と果てしない暗黒だけだ
第20段落 (文字数 496): 突然低い重しいオルガンの調べがひびきはじ ~中略~ まにまに空高く浮びあがるような気さえする
第21段落 (文字数 90): わたしは音楽がときとして湧きおこしがちな ~中略~ くの時計がしずかに暮れてゆく日をしらせた
第22段落 (文字数 942): わたしは立ちあがって寺院を去る支度をした ~中略~ 少かれ辱かしめられ不名誉を蒙っているのだ
第23段落 (文字数 253): 一日の最後の光が今やわたしの頭上の高い円 ~中略~ て閉まり建物全体にこだまして鳴りわたった
第24段落 (文字数 859): わたしは今まで見てきたものを心のなかで少 ~中略~ 王のミイラは鎮痛剤として売られている原註
第25段落 (文字数 333): 今この大建築はわたしの上にそびえ立ってい ~中略~ 廃墟となるのである原註トマス・ブラウン卿

全ての作品を段落に分けて、それぞれに著者や文芸思潮などの属性を付けたデータセットを作成します。

from tqdm import tqdm

# 段落で分けたデータセットを格納する辞書
dict_para = {}
for idx in tqdm(df.loc[~df.group.isnull(), :].index):
  # データセットに含めたい情報
  title = df.loc[idx, "作品名"]
  date = df.loc[idx, "date"]
  author = df.loc[idx, "author"]
  group = df.loc[idx, "group"]
  li_group = df.loc[idx, "li_group"]
  # 本文を段落ごとに分割する
  text = df.loc[idx, "text"]
  for num, para in enumerate(get_paragraphs(text)):
    # 抽出した段落の長さが一定以上の場合はデータセットとして格納
    if len(para) > 2:
      dict_para["{}_{}".format(idx, num)] = [idx, title, date, num, para, author, group, li_group]
# 作った辞書からDataFrameを作成
df_para = pd.DataFrame.from_dict(
    dict_para,
    orient="index",
    columns = ["title_ID", "title", "date", "num_paragraph", "paragraph", "author", "group", "li_group"]
    )
# 各段落の文字数をカウント
df_para["len_paragraph"] = df_para["paragraph"].map(lambda x: len(x))

段落の文字数をカウントすると以下のようになります。区切り文字の関係でうまく分割できていない作品もあるようですが、概ね1000文字以内の長さに収まっています。

df_para.len_paragraph.describe()
len_paragraph
count 218974.000000
mean 155.212199
std 519.755551
min 3.000000
25% 47.000000
50% 94.000000
75% 177.000000
max 97387.000000
pip install japanize_matplotlib
import matplotlib.pyplot as plt
import japanize_matplotlib

fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111)
# グラフの見やすさのために1500以上の長さのデータは除いてプロットしています
df_para.loc[df_para.len_paragraph<1500, "len_paragraph"].hist(bins="auto", ax=ax)
ax.set_xlabel("段落の文字数")
ax.set_ylabel("頻度")
本文から取り出された各段落の文字数のヒストグラム

本文から取り出された各段落の文字数のヒストグラム

 

おわりに

ここからさらにモデルのアーキテクチャに合わせて細かいデータの前処理を行う必要はありますが、なんとかそれらしいデータセットができたのでこの記事はこの辺で終わります。この後は作ったデータセットをクラスタリングしたり、学習データとして分類モデルを作成したりしてみます。何か面白い結果が出たらまた報告します。