超長文をChatGPTに要約させてみる

# はじめに
128kトークンを扱えるgpt-4-turboが出てきました。これで長文の要約も1発かなと思いきや、やっぱり欲しい情報粒度を調整したいとか、検索もしたいとか、あるいはもっと長文をいれたいとかいろいろあると思いますので、今回は長文の要約を幾つかのパターンで検証してみたいと思います。

やったことは以下の3つのパターンでの長文要約です。

**(1) 長文をブロックにわけて単純要約を繰り返し、最終要約を作成する (多段階の単純要約)**
多段階に要約を積み重ねていき、最終的にはとりまとめ要約で合体させるパターンです。なお、この方式の場合入力文章に上限はありません。超長文に対応です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/935f9f27-f331-dd8c-a997-f18aaf40cc7b.png)


**(2) 長文を128kトークンを活用して、1回の単純要約で最終要約を作成する (単発の単純要約)**
全文を一回で単純に要約するパターンです。入力トークンの上限に依存します。今回のケースだと128kが要約できる上限になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/94ab00f9-1a1a-d03b-4ab3-1fcf9ff45f54.png)


**(3) (2)の方式にプロンプトエンジニアリングを加えて最終要約を作成する（単発の工夫要約）**
単純要約だけだと長文の場合意図をとれなくなると思い、プロンプトエンジニアリグを駆使()してちょっと工夫した要約にしたパターンです。こちらも上限は128kになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/7c993465-b46b-639c-1e2d-deb4644ce80f.png)


なお、今回の要約対象としたのは、太宰治の「人間失格」です。これ大体82kトークンくらいなので長文として扱いやすかったのが決め手です。超長文ではないかもしれませんが、(2),(3)との結果比較もできるのでこちらにしました。

またモデルも同じもので比較したかったので全てgpt-4(1106-preview)を使いました。(1)の方式だけであれば極端な話gpt-35-turboの初期モデルでも対応可能です。

# とりあえず結果
詳細はこの後に記載していくとして、まずはざっくりこんな結果でしたという表です。
128k以下の長文であっても、(1)の方式は業務文書などには特に有効そうでした。一方で全体感のある文章把握は(3)の方式が優れているため、128k以下であれば(1)と(3)を組み合わせると良さそうです。
なお、対象文章が128kトークンを超える場合は(1)方式をとるしかないですし、将来的に入力トークンがより大きくなってもまだまだ(1)スタイルの価値は無くならそうです。

**業務文書に使うなら(1)の方式、小説のカバーとかを書くなら(3)の方式がよさげです。**

|  |(1)多段の単純要約|(2)単発の単純要約|(3)単発の工夫要約|
|---|:-:|:-:|:-:|
|長文対応性|〇(無限)|△(128kまで)|△(128kまで)|
|検索対応|〇(途中成果物を検索対象にできる)|✕|✕|
|お手軽さ|✕|〇|〇|
|全体把握|△|〇|〇(特に有効)|
|コメント|事実の羅列になりがち。業務文書などで検索と合わせての利用が有効そう|プロンプト工夫しないと微妙かも、出力文章量も少ないのでお試しにはいいけど…|全体としての把握が容易になる、プロンプトの工夫次第で使いまわしも良さそう|

# (1)多段の単純要約について
(1)多段の単純要約は以下図のような形です。冒頭の図とほぼ同じことを書いていますが、検索で使われるchunkとoverlapをつかって要約文書の洗練化を図っています。特に前段の要約を加味して要約を作成することで無駄な要約を極力排除します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/941cfca2-e66f-6b5f-3089-498109ff1230.png)

この方式ではいくつかパラメータとなりうる項目（**太字**)があります。
- 要約の試行回数: n (最初は0)
- 要約対象の文書全体量（トークン）: **T(n)**
- 一度要約をしたときの文書全体量（トークン）: **T(n+1)**
- 最終的な要約に期待する文書量（トークン）: **Te**
- 対象文章の要約時の標準的な出力文章量（トークン）: **s**
- 個別要約の対象チャンク(トークン): **c**
- 前段部分のオーバーラップ（トークン）: **overlap**

今回やりたいことをざっくり式で表すと以下になります。

```math
T(n+1) = \lceil \frac{T(n)}{c} \rceil \cdot s
```

T(n+1)がTeを下回るまで要約を繰り返し、下回ったらT(n+1)を結合するような形で要約を作成させてほしい結果を得る感じです。必要なパラメータは以下のように導出していきます。
(でもまぁパラメータ導出したら何回くらいの試行で要約できそうかがわかるくらいで、結局全然厳密ではないので、この式は雰囲気で見てもらえたら幸いです。)

## パラメータの決定
### 1. 全体の文章量を図る: T(0)
tiktokenなどをつかって、ざっくりとした全体トークン数をカウントします。
今回の対象である「人間失格」はgpt-4で計算すると82.6kトークンでした。

```python
import tiktoken

encoding_4 = tiktoken.encoding_for_model("gpt-4")
encoding = tiktoken.get_encoding(encoding_4.name)
encoded = encoding.encode(text)

print("トークン数: ", len(encoded))
```

```math
T(0) = 82.6k (token)
```

### 2. 最終的に欲しい要約の文書量を決める

今回は800トークンくらいの文章が最終的に欲しいかなとしました。だいたい原稿用紙2枚分ないくらいですね。ここはえいやでパッと見れるくらいのあなたの気持ちのいい数字を選びます。ただあんまり短いと単発要約との差もわかりづらくなるので、ある程度内容を入れれる量がおすすめです。

```math
Te = 800 (token)
```

### 3. 対象の文章の要約出力のトークン数を確認する

特にフォーマットなどを指定せずに要約させると、「人間失格」の文章は、要約時の入力トークン数によらずだいたい300トークンくらいの要約が出力されました（下図）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/ea753675-61af-430d-0e4d-41bcec749925.png)

なので、ここもえいやです。（ちゃんとやるならもう少し計算させて、ですけどLLMの出力がどうしたって揺れるのでえいやがオススメです。）
```math
s = 300 (token)
```

1回の要約で大体300トークンが出てくるので、欲しい要約量(800トークン)をみると、要約を2つ～3つくらいまで絞れれば欲しい量の要約が出そうだということがわかります。

### 4. 要約対象のチャンクとオーバラップを決める

ここは精度見ながら決めていくところです。チャンクサイズを小さくすると要約する数が増えますが小さな粒度で文書検索できるようになりますし、大きいと要約する数が減りますが細かい話が省略されてしまいます。
とりあえずチャンクサイズとして4000トークン、オーバラップを1000としてみます。

```math
c = 4000 (token) 
```
```math
overlap = 1000 (token)
```
このあたりは人間チューニングするしかないかなと思います。えいやで決めて結果をみて試行錯誤ですね。（c(0)=4000、c(1)=2000とか、要約試行回数を加味してこのあたりのパラメータを調整するとよさそうでした。けど、今回は省略。）

パラメータを決めた上で、以下のように文章をチャンク単位で分割します。`encoded`はトークンを計算したときに計算したものを使ってます。

```python
chunk_size = 4000
over_lap = 1000

# calculate the number of chunks
num_chunks = len(encoded) // chunk_size + 1

# create empty list
chunks = []
lengths= []

for i in range(num_chunks):
    start_idx = i * chunk_size
    if(start_idx > 0):
        start_idx -= over_lap
    end_idx = (i + 1) * chunk_size
    encoded_chunk = encoded[start_idx:end_idx]
    #decoded
    chunk = encoding.decode(encoded_chunk)
    chunks.append(chunk)
    lengths.append(len(encoded_chunk))
```


## 要約させるシステムプロンプト
今回は小説をターゲットに要約させるので以下の感じでまるっと単純要約しましたが、ここも欲しい要素をきちんと入れ込む工夫をしたプロンプトが理想です。

ポイントは、前段の要約がある場合は、オーバラップ部分にダブる要素がでてくるので、そこを排除させるようなプロンプトにしているところです。

```python
    system_prompt = "あなたは小説の熟練の編集者である。小説の一部分が入力されるので、\
    小説の内容を簡潔な日本語で要約する。"

    #pre_partが存在する場合、それを前段の要約として追加する
    if pre_part != "":
        system_prompt = f"""{system_prompt}
また入力された小説の前段の要約は以下の通りである。この前段の要約も参考にし、\
前段の要約に含まれる部分は要約に含めないようにする。

前段の要約:
{pre_part}
"""
"""
```
user_promptは chnuk化された文章をいれます。

## 要約していく
ちょっと適当ですが、以下のような感じで要約した結果を追加していきます。(Copilot省略化)
```python
summaries_1 = []
for i in range(num_chunks):

    #chunks[i-1]が存在するなら引数に追加する
    if i > 0:
        summary = makeSummary(chunks[i], summaries_1[i-1])
    else:
        summary = makeSummary(chunks[i], "")

    summaries_1.append(summary)
```
今回のケースだと21回の4000トークンの要約が実行されます。環境によっては長時間処理でエラーになったりOpenAI 側からレートエラーが返ったりとあるかもしれませんので、エラーハンドリングと待機処理、ループ処理はお忘れずに。

大体3分で以下のように1次要約がそろいました。（合計文章量T(1)=6585トークン)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/ee4d2f45-11db-b0d1-797b-259e7942d966.png)

## 再度要約する
1次要約の結果は6585トークンの文章量だったので、求めている要約トークン数800を超えているため、再度要約を実行します。が、2回目以降の要約は要約の要約になるのでプロンプトをちょっと変えます。
(本当は2回目以降でchunkサイズとかoverlapも変えると良さそうですけど、今回はそこまでやりませんでした）

```python
    system_prompt = "あなたは小説の熟練の編集者として、\
    小説の各パートの要約をまとめた文章を受け取り、再度その内容を要約します。"

    #pre_partが存在する場合、それを前段の要約として追加する
    if pre_part != "":
        system_prompt = f"""{system_prompt}
また前段の要約は以下の通りです。前段の要約も参考にし、\
前段の要約に含まれる部分は要約に含めないようにして下さい。

前段の要約:
{pre_part}
"""
```

2次要約した結果、2個の要約文章（合計624トークン)ができました。2回目の要約で求めているトークン数(800)を下回りましたので、最後に結合要約させます。

## 要約結果をとりまとめる

GPTの要約文章って改行なくて読みずらいので、最後に整理を兼ねて整形させます。(以下のプロンプト)

```python
system_prompt = "あなたは小説の編集者です。\
小説の各パートの要約をまとめた文章を受け取り、\
読みやすく改行を適切に入れた形で文章を整形してください。"
```

最終的には以下のような要約が完成しました。でもネタバレになるので折りたたみしておきます。
<details><summary>どんな要約ができたか気になる方はココをクリック（ネタバレ注意）</summary><div>

『人間失格』は、主人公が自身の人生を振り返り、人間としての苦悩を語る物語である。
幼少期から成人に至るまでの外見と内面の変化を序章で示し、その後の手記で生い立ちや日常生活への違和感、人間関係の困難さを明かす。
家族との関わりや性的虐待の経験、人間不信や孤独感を抱えながらも、道化を演じて生きることを選ぶ。
学校生活や女性との複雑な関係、左翼思想や非合法活動への関わりを経て、金銭的困窮や情死事件を経験する。
最終的には自殺未遂を起こし、生家との関係が断絶される可能性に直面する。
ヒラメの家で孤独な生活を送りながら、将来についての不安と人との関わりに苦痛を感じる心情が描かれる。
主人公は自殺未遂後、ヨシ子との関係を終わらせる決意を固め、ヒラメとマダムに看護される。
モルヒネの注射に依存し、病院で狂人として扱われるが、自分は狂っていないと主張する。
故郷の長兄が父の死を伝え、主人公は田舎で療養生活を始める。
三年後、薬屋で下剤を間違えて買うが、笑い飛ばし、幸福も不幸もないと感じる。27歳になり、白髪が増える。
あとがきでは、手記を書いた狂人について語られ、筆者は手記を雑誌社に発表することを考える。マダムは手記の葉ちゃんが素直でよく気が利く人物だったと述べる。

</div></details>

**ネタバレ結果を見たくない方向けに解説するとすると、「人間失格」の内容を記載内容が要約して羅列している感じです。小説の良さみたいなのは省かれてしまっています。**


# (2)単発の単純要約について
今回の対象である「人間失格」は約82kトークンでしたので、gpt-4-turboなら全文の投入が可能です。以下のような単純なプロンプトで一括要約させてみます。

```python
system_prompt = "あなたは小説の熟練の編集者である。\
小説の全文が入力されるので、小説の内容を簡潔な日本語で要約する。"
```

こちらもネタバレになるので折りたたみます。要約としては262トークンの文章量になりました。（大体300トークンですね）
<details><summary>どんな要約ができたか気になる方はココをクリック</summary><div>

太宰治の「人間失格」は、主人公の手記を通して、彼の生涯と内面の葛藤を描いた作品である。
幼少期から病弱で、人間関係においても常に他者との距離を感じ、自己嫌悪に苛まれる。
成長するにつれ、酒や女性、薬物に溺れ、自身の存在意義や人間としての資格を問い続ける。
最終的には、自分の罪悪感と不安から逃れるために自殺を図るが、失敗し、精神病院に収容される。
自分を取り巻く世界との断絶を感じながら、孤独と苦悩の中で生きる主人公の姿が描かれている。

</div></details>


**ネタバレ結果を見たくない方向けに解説するとすると、「人間失格」のほんとふわっとした要約になっています。”あらすじ”に使えないことはないかな？というレベルの内容の要約になっています。**

# (3)単発の工夫要約について
(2)では指示も単純で出力の文章量も少なかったので少しCoTみたいな指示を追加します。この辺は色々と工夫の余地はありそうです。けど、このくらいでも体感の精度は上がった感じがしました。

```python
system_prompt = f"""あなたは小説の熟練の編集者である。\
小説の全文が入力されるので、小説の内容を簡潔な日本語で要約する。\
要約は10個のパートに分割して出力する"""
```

こちらもネタバレになるので折りたたみます。要約としては328トークンの文章量になりました。（大体300トークンですね）。しっかしgpt-4-turboになっても日本語の文章量指定はむつかしいですね・・・
<details><summary>どんな要約ができたか気になる方はココをクリック</summary><div>

太宰治の『人間失格』は、主人公の手記として書かれた小説である。
主人公は幼少期から病弱で、人間の営みについて理解できず、周囲とのコミュニケーションに苦悩する。
成長するにつれ、道化のような振る舞いで人間関係を築くが、内面では孤独と不安に苛まれる。
学生時代は美貌で注目されるが、本質的な孤立感は変わらず、大人になっても人間関係や社会生活に適応できない。
酒や女性、左翼運動に関わりながらも、自己嫌悪と罪悪感に苛まれる。
最終的には自殺未遂を経て精神病院に入院し、人間としての資格を失ったと感じる。
自分の存在が罪であると結論づけ、人間失格としての人生を受け入れる。

</div></details>

**ネタバレ結果を見たくない方向けに解説するとすると、「人間失格」のなにやらいい感じの要約になっています。”あらすじ”に使えそうかも？というレベルの内容の要約になっています。(ちょっとネタバレ要素が強めですが)**

# まとめ
結局(1)~(3)は以下の結果になりました。(1)の形式は途中成果物の要約を検索対象とすることで最終要約結果から、部分部分の要約元文章に遡れるので業務文書との相性はとても良いように感じます。
一方全体を見比べると(3)が正しく文章を全体把握しているように見えます。特に(3)は、小説の意味をある程度分かる感じの要約になっていました。やっぱり(1)は細切れをみているのでどうしても全体把握感は下がりますね。

|  |(1)多段の単純要約|(2)単発の単純要約|(3)単発の工夫要約|
|---|:-:|:-:|:-:|
|長文対応性|〇(無限)|△(128kまで)|△(128kまで)|
|検索対応|〇(途中成果物を検索対象にできる)|✕|✕|
|お手軽さ|✕|〇|〇|
|全体把握|△|〇|〇(特に有効)|
|コメント|事実の羅列になりがち。業務文書などで検索と合わせての利用が有効そう|プロンプト工夫しないと微妙かも、出力文章量も少ないのでお試しにはいいけど…|全体としての把握が容易になる、プロンプトの工夫次第で使いまわしも良さそう|

ちなみに、(1)の方式でプロンプトエンジニアリングする余地は多々ありそうです。（例えば文書のタイトルなんかを意識させるなど）
また、今回検証でつかった「人間失格」は、もともとのGPTの学習データに含まれていると思われますので、学習元データに入ってない文章で試してみたいところです。

今回長文の検証を比較したわけですが、全体把握できる要約文章から、パートごとの要約文章、さらにそこから中間要約文書(1)->中間要約文章(2)、そして最後は元文章みたいな形でたどれると文書把握のスピードはあがりそうなのでそういうツールはありかもと思いました。というところで今回の検証は以上です。