【Python】T5でクソデカ変換を試みたけど難しかった

# 概要
[クソデカ羅生門](https://anond.hatelabo.jp/20200611125508)風の文章を生成したくなったので、T5（text-to-text transfer transformer）による変換を試しましたが、あまりうまく行きませんでした。
やったことのメモを残しておきます。

# 目的
普通の文章をクソデカ文（後述）に変換する変換器をつくることが目的です。

# 背景

[クソデカ羅生門](https://anond.hatelabo.jp/20200611125508)は『羅生門』（芥川龍之介）の語彙を過剰に強調することでおかしみを生じさせている文章です。このような表現が過剰に強調された文章を本記事ではクソデカ文と呼びます。

例えば原文「一人の下人が、羅生門の下で雨やみを待っていた。」はクソデカ文では「一人の下人が、`クソデカい`羅生門の`完全な真`下で雨やみを`気持ち悪いほどずっと`待`ちまく`っていた。」となります。

# 先行事例
[「クソデカ羅生門」的な文章を生成したかった(過去形)](https://qiita.com/gaqwest/items/03d66b00ba1152d81441)にて、ルールベースの変換が検討されています。かなりそれっぽいものができています。一方で、「亜音速」のような本家にときおり現れる芸術度の高い強調表現をルールだけで再現するのは限界があるように思われたので、機械学習の力を借りてなんとかならないか、検討してみました。

# 方針
テキスト変換に適した事前学習モデルをファインチューニングすることで、原文とクソデカ文の変換器を作ります。事前学習モデルとして、sonoisa様の[日本語T5モデル](https://huggingface.co/sonoisa/t5-base-japanese
)T5 (text-to-text transfer transformer）を使いました。

# 学習データ
羅生門の原文とクソデカ文の対応する文章ペアを学習データとします。

## 原文
原文は[青空文庫](https://www.aozora.gr.jp/cards/000879/card127.html#download)からダウンロードします。冒頭と末尾の紹介文的な要素を手動で削除したあと、ルビ情報を正規表現で削除します。ルビ情報を削除するコードは以下です。

```Python:remove_rubi.py
import re
import argparse
from pathlib import Path


def preprocess(text):
  exp = "｜|《[^《》]*》|※［[^［］]*］"
  return re.sub(exp, "", text)

def getArgs():
  parser = argparse.ArgumentParser()
  parser.add_argument("input")
  parser.add_argument("-o", "--output", default = None)
  return parser.parse_args()

if __name__ == "__main__":
  args = getArgs()
  input_path = args.input
  default_output_path = Path(input_path).parent.joinpath(f"preprocessed_{Path(input_path).name}")
  output_path = args.output or default_output_path
  
  with open(input_path, encoding="shiftjis") as f:
    raw_text = f.read()
  preprocessed_text = preprocess(raw_text)
  with open(output_path, "w") as f:
    f.write(preprocessed_text)
```

以下のように実行します。

```
python remove_rubi.py input.txt output.txt
```

## クソデカ文
クソデカ文は[クソデカ羅生門](https://anond.hatelabo.jp/20200611125508)をコピペします。

## ペアの作成
原文とクソデカ文をそれぞれ「。」で分割して、先頭からシンプルに対応付けます。
ひょっとしたら「。」の数がずれているかもしれないので、スプレッドシートに貼り付けて、念のため、目視で確認します。エクセルに貼り付けやすいように`pyperclip`でクリップボードに貼り付ける処理も入れています。

ついでに以下の処理も入れています。

* `neologdn`による正規化
* 空白の削除
* 不自然なカギカッコの削除（セリフの途中に句点が存在することがあるため）

```Python:split_and_copy.py
import re
import argparse
from pathlib import Path
import pyperclip
import neologdn

def getArgs():
  parser = argparse.ArgumentParser()
  parser.add_argument("input")
  return parser.parse_args()

def split(text, sep="。"):
  lines = []
  for line in text.split("。"):
    line = "".join(line.split())
    line = neologdn.normalize(line)
    if not line: continue
    if line.startswith("「") and not line.endswith("」"):
      line = line[1:]
    if line.startswith("」"):
      line = line[1:]
      
    line += "。"
    lines.append(line)
  return lines
    
def copy(lines):
  pyperclip.copy("\n".join(lines))
  
if __name__ == "__main__":
  args = getArgs()
  input_path = args.input
  
  with open(input_path) as f:
    raw_text = f.read()
  lines = split(raw_text)
  copy(lines)  
```

以下のように実行すると、原文、クソデカ文それぞれに対し実行し、実行のたびに結果をスプレッドシートに貼り付けます。今回は幸い、句点の数が完全に一致するようで、段ズレは生じませんでした。列名を手動で付与後、結果をcsvに出力し、最終的に以下を得ました。

```:rashomon_train.csv
original,kusodeka
ある日の暮方の事である。,ある日の超暮方(ほぼ夜)の事である。
一人の下人が、羅生門の下で雨やみを待っていた。,一人の下人が、クソデカい羅生門の完全な真下で雨やみを気持ち悪いほどずっと待ちまくっていた。
広い門の下には、この男のほかに誰もいない。,馬鹿みたいに広い門の真下には、この大男のほかに全然誰もいない。
ただ、所々丹塗の剥げた、大きな円柱に、蟋蟀が一匹とまっている。,ただ、所々丹塗のびっくりするくらい剥げた、信じられないほど大きな円柱に、象くらいある蟋蟀が一匹とまっている。
...
```

# T5のファインチューニング
[PytorchのTransformersのT5を使って要約モデルを作る | 見習いデータサイエンティストの隠れ家](https://www.dskomei.com/entry/2021/11/12/103050)
にて紹介されているT5のファインチューニング用コードをほぼそのまま使いました。

変更点として、GoogleColabで実行したのですが、batch_size=8だとout of memoryになったため、batch_size=4にしました。入出力のトークンサイズは参考元は入力のほうが大きかったのですが、クソデカ化によって出力のほうが入力よりも大きくなると想定されるため、max_length_src = 200、max_length_target = 400としました。

学習はだいたい2,30分ほどで終わりました。

# 結果
検証用データに含まれる原文を適当にピックアップして生成し、正解と比べてみます。
「正解」と「生成」の差分を太字で示します。

|本文| 正解| 生成|
|---|---|---|
|その老婆は、右の手に火をともした松の木片を持って、その死骸の一つの顔を覗きこむように眺めていた|`その`老婆は、右の手に`大`火炎をともした`最高級`松の`巨大`木片を持って、その`大`死骸の一つの`巨`顔を覗きこむように眺め`倒し`ていた|老婆は、右の手に火をともした松の`大`木片を持って、その死骸の一つの顔を覗きこむように眺めていた|
|しかし下人にとっては、この雨の夜に、この羅生門の上で、死人の髪の毛を抜くと云う事が、それだけで既に許すべからざる悪であった。|しかし`馬鹿`下人にとっては、この`豪`雨の`聖`夜に、この`クソデカ`羅生門の真上で、`大`死人のぬばたまの髪の毛を抜くと云う事が、それだけで既に`絶対に`許すべからざる`世界最低の悪の中の`悪であった|`韋駄天の異名をとる`下人にとっては、この雨の夜に、この`クソデカい`羅生門の上で、死人の髪の毛を抜くと云う事が、それだけでもう許すべからざる悪であった|
|今この下人が、永年、使われていた主人から、暇を出されたのも、実はこの衰微の小さな余波にほかならない。|今この`最強にヤバい`下人が、永年、`犬のごとくこき`使われていた主人から、暇を出されたのも、実はこの`大衰微のクソしょぼい小さな`小さな余波にほかならない|あの下人が、永年、使われていた主人から、暇を出し倒されたのも、実はこの`超巨大`余波にほかならない|

# 考察
## 生成文の定性評価
正解文で「巨大木片」だったものが生成文では「大木片」となっているなど、生成文は正解文に比べて、変換が控えめな傾向でした。また変換の箇所も、1文につき多くて1、2箇所と正解文に比べてかなり少なめでした。また、入力と全く同一の文章が出力されるケースもそれなりにありました。
今回は152ペアしか学習データが用意できず、大量の「普通の」日本語で事前学習されたモデルにクソデカ文のような極端なデータを出力させるには、不十分だったのかもしれません。

生成文では原文に存在した用語が消失するケースがあり、原文に純粋に要素を追加することによってのみ行われるというクソデカ化のルールがやぶられています。また原文の「小さな余波」を「超巨大余波」としたように、原文の意味を逆にしているケースも見られました。原文のルールを守らせる何らかの工夫があったほうが良いと考えられます。

一方で、`韋駄天の異名をとる下人`のようにルールベースでは生成が難しそうな表現もたまに見られ、機械学習ベースの可能性も感じさせました。

## 今後の課題
学習データを増やすことで精度向上が期待できます。そのために
* 羅生門以外の物語のクソデカ文を利用する。本家と異なる作者であるが、羅生門以外のクソデカ文がいくつか公開されている。
* 文章単位ではなくフレーズ単位で学習する
* 文章生成モデルによって学習データを拡張する

などの方法が考えらます。

T5以外の事前学習モデルで生成に適したものがないかも探してみたいです。GPT系など文章生成に適したモデルのほうが原文から飛躍した変換を行いやすいかもしれません。ルールベースで変換したあとの微調整に機械学習を用いることも考えられます。