【2023ver.】ジャルジャルのネタのタイトル生成する奴

2021年8月に、私が大好きなお笑いコンビ「ジャルジャル」に関する、以下のような記事がQiitaに投稿されています。

https://qiita.com/masaka_programming/items/7f2db9a77c18c0000dc2

もともとジャルジャルというお笑いコンビが好きだったこともあり、記事が投稿された頃にはとても興味深く読ませていただいたのを覚えています。

当該記事では、YouTubeに投稿されているジャルジャルのコント動画のタイトルを収集し、マルコフ連鎖により"架空のタイトル"を生成しています。

それから約1年8ヶ月後の現在、ChatGPTをはじめとする大規模言語モデルが隆盛を極めています。
私は、「__大規模言語モデルを使えば、もっと自然な日本語で、もっとバリエーションに富み、もっと面白いタイトルが生成できるのでは？__」と考えました。

先行研究におけるマルコフ連鎖によるタイトル生成では、以下のような課題があります。
- 学習データに含まれる単語しか生成できない。例えば『Qiitaにお笑いの記事投稿する奴』のようなタイトルは、（学習データに「Qiita」という単語がないので）原理的に生成不可能。
- 長い文脈を考慮できない。
    - 先行研究では前2単語から次の単語をサンプリングしている。（2階マルコフ連鎖）
    - 仮にマルコフ連鎖の階数を増やしても、学習データセット（ネタのタイトル）中の単語の繋がりしか考慮されず、自然な日本語を生成する保証は少ない。

これらの課題を、 __日本語データセットで事前学習した大規模言語モデル__ を使うことで克服し、より自然で、バラエティに富み、面白いタイトルを生成することを目指します。

# やること
- ジャルジャルのYouTubeチャンネル[『ジャルジャルタワー JARUJARU TOWER』](https://www.youtube.com/channel/UChwgNUWPM-ksOP3BbfQHS5Q)から投稿動画のデータを収集し、ネタのタイトルを抽出
- 抽出したタイトルを学習データとして、日本語版GPT-2をファインチューニング
- チューニングしたGPT-2を使用して、新たなタイトルを生成！

# 手順
## YouTube Data API経由のデータ収集
最初に、[YouTube Data API](https://developers.google.com/youtube/v3/getting-started?hl=ja)を使ってネタのタイトルを自動収集します。
なお、筆者が収集したデータは2023年5月4日時点のものです。

事前にAPI keyを発行しておきます。以下などが参考になりました。

- https://qiita.com/shinkai_/items/10a400c25de270cb02e4
- https://zenn.dev/eito_blog/articles/f2d870ffddb636

Python用にGoogleのAPIを操作するためのライブラリが用意されているので、pipでインストールします。

```
pip install google-api-python-client
```

API経由で情報を取得するコードを書きます。
一度のリクエストで取得できる件数の上限は50件で、それ以上取得したい場合はレスポンスから`nextPageToken`を取り出し、次のリクエストに含める必要があります。

```python
import time
from apiclient.discovery import build

YOUTUBE_API_KEY = 'APIキーをコピペ'
youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY)

def list_all_videos(channelId, sleeptime=3, **kwargs):
    '''
    指定したチャンネル内の動画を全件取得する
    '''

    items = [] # 結果を格納するリスト
    
    req_params = {
        'part': 'snippet',
        'channelId': channelId,
        'order': 'date',
        'maxResults': 50
    }
    for kw in kwargs:
        req_params[kw] = kwargs[kw]
    
    search = youtube.search()
    req = search.list(**req_params)
    
    while True:
        # リクエストを実行し、結果をitemsに追加
        res = req.execute()
        items += res['items']
        
        # 次ページの検索結果が存在しなければ、ループを抜ける
        if not 'nextPageToken' in res:
            break
            
        # 次ページ用のリクエストを作成する
        req = search.list_next(req, res)
        print(f'{len(items)} videos have been found...')
        time.sleep(sleeptime)
        
    print(f'{len(items)} videos have been found!')
    
    return items
```

あとは定義した関数にチャンネルIDを入力してデータを取得すれば良いわけですが、__取得件数が500を超えるあたりで`nextPageToken`が返ってこない__ ことが判明しました。そのため、取得件数が500を超えないように、期間を絞って検索を行うこととしました。検索対象期間の指定には、`publishedAfter`、`publishedBefore`を指定します。

```python
# チャンネルID (YouTube上のチャンネルに付与される識別子)
# ジャルジャルタワー JARUJARU TOWER (https://www.youtube.com/channel/UChwgNUWPM-ksOP3BbfQHS5Q)
channelId = 'UChwgNUWPM-ksOP3BbfQHS5Q'

# 2018~2023年の動画を半年ごとに取得
years = list(range(2018, 2024))
terms = []
for year in years:
    terms += [
        [f'{year}-01-01T00:00:00Z', f'{year}-06-30T00:00:00Z'],
        [f'{year}-07-01T00:00:00Z', f'{year}-12-31T00:00:00Z'],
    ]

# publishedAfter, publishedBeforeで期間を指定しつつ情報取得
items = []
for term in terms:
    items += list_all_videos(
        channelId,
        publishedAfter=term[0],
        publishedBefore=term[1]
    )
```

## データセット作成
上記でダウンロードしたデータを整形し、ファインチューニングに使用します。

```python
# 取得データをCSVに保存する

# 動画タイトル、投稿日時、videoId
data = []
for item in items:
    if not item['id']['kind'] == 'youtube#video':
        # 検索結果に含まれるプレイリストを除外するための処理
        continue
        
    data.append({
        'title': item['snippet']['title'],
        'publishedAt': item['snippet']['publishedAt'],
        'videoId': item['id']['videoId']
    })
    
df = pd.DataFrame(data)
# 投稿日時でソートして保存する
df.sort_values('publishedAt').to_csv('jarujaru_titles.csv', 
                                     encoding='utf-8_sig', 
                                     index=False)
```

学習データの形式は、各行に1つずつネタのタイトルが記載されたテキストファイルとします。

また、タイトルの前後に文の開始/終了を示すBOSトークン`
`、EOSトークン`
`を挿入します。

```python
df = pd.read_csv('jarujaru_titles.csv')
titles = df['title'].to_list()

# 動画タイトル中の『』で囲まれた部分を抽出する
pattern = r'『.+』'

text_train = ''

for title in titles:
   # 正規表現で『』の間にあるネタのタイトルを取得
    matches = re.findall(pattern, title)
    if len(matches) != 1:
        continue
    conte_title = matches[0].strip('『』')

    # BOS/EOSトークンを挿入
    text_train += '
' + conte_title + '
' + '\n'

# テキストファイルに保存
with open('train.txt', mode='w', encoding='utf-8') as f:
    f.write(text_train)

print(len(text_train.split('\n')))
# >>> 2135
```

最終的に、以下のようなテキストファイルが完成しました。収集できたネタ数は2,135個でした。
```text:train.txt
...

密着ＯＫ、カメラＮＧな奴


入院してる病室教えへん奴


男子バレエ部に入った奴


勇気、持続せーへん奴


言語覚える天才な奴

...
```

余談ですが、ジャルジャルは __1日1本のペースでネタを投稿しており、しかもタイトルは『〜奴』で統一__ されています。
この __データ量とフォーマットの良さ__ から、お笑い系NLPer（？）には扱いやすいデータセットと言えるかもしれません。

## ファインチューニング

事前学習済みの大規模言語モデルとして、日本語版GPT-2をファインチューニングします。以下の記事を参考にしています（というかほとんどそのまま使わせていただきました）。

https://qiita.com/m__k/items/36875fedf8ad1842b729

使用したモデルは、rinna社による日本語版GPT-2（[https://huggingface.co/rinna/japanese-gpt2-medium](https://huggingface.co/rinna/japanese-gpt2-medium)）です。Google Colabの無料GPUで動かせます。

最初に必要ライブラリをインストールします。
```shell
# 必要なライブラリをインストール
pip install git+https://github.com/huggingface/transformers
pip install sentencepiece
pip install datasets

# transformersレポジトリ内のソースコードを実行したいので、カレントディレクトリにもクローンする
git clone https://github.com/huggingface/transformers

# バージョンにもよると思うが、筆者の場合は以下の記述が必要だった
pip install evaluate
pip install --upgrade accelerate
```

先程の`train.txt`を訓練データとしてファインチューニングします。
`transformers`内の`run_clm.py`に適当な引数を与えて実行するだけです。めっちゃ簡単。

```shell
python ./transformers/examples/pytorch/language-modeling/run_clm.py \
    --model_name_or_path=rinna/japanese-gpt2-medium \
    --train_file=drive/MyDrive/ColabNotebooks/jaru/train.txt \
    --validation_file=drive/MyDrive/ColabNotebooks/jaru/train.txt \
    --do_train \
    --do_eval \
    --num_train_epochs=100 \
    --save_steps=10000 \
    --save_total_limit=3 \
    --per_device_train_batch_size=1 \
    --per_device_eval_batch_size=1 \
    --output_dir=drive/MyDrive/ColabNotebooks/jaru/output/ \
    --use_fast_tokenizer=False
```

少し過学習気味でも良いかと思ったので、（多めのつもりで）エポック数は100に設定しています。
処理完了までは1時間程度でした。今回は「短いテキスト×2000強」という程度のデータセットだったので、思ったよりも学習が早く終わりました。

## 生成
ファインチューニングしたモデルを読み込み、実際にネタのタイトルを生成してみます。
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# モデルの準備
model = AutoModelForCausalLM.from_pretrained("drive/MyDrive/ColabNotebooks/jaru/output/").to(device)
model.eval()

# トークナイザの準備
tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt2-medium")
tokenizer.do_lower_case = True

# 入力文字列を指定し、テキストを生成させる
# 生成する数はnum_return_sequencesで指定する
input_text = 'プログラミングするときに'
input_ids = tokenizer.encode(input_text, return_tensors='pt').to(device)
out = model.generate(input_ids, do_sample=True, top_p=0.95, top_k=30,
                    num_return_sequences=3, max_length=64, bad_words_ids=[[1], [2]])
out_decoded = tokenizer.batch_decode(out, skip_special_tokens=True)

for i in range(3):
    print(out_decoded[i])
```

##### 出力

```
The attention mask and the pad token id were not set. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results.
Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.
A decoder-only architecture is being used, but right-padding was detected! For correct generation results, please set `padding_side='left'` when initializing the tokenizer.
プログラミングするときにundo 文で文を切り替える奴
プログラミングするときにint を使うかどうか聞いてくる奴
プログラミングするときに‘; を使わない奴
```

「プログラミングするときに」という入力（プロンプト）を与え、3つのテキストが生成されました。
__「プログラミング」なんて語彙は学習データには含まれないにも関わらず、確かにジャルジャルのネタっぽいテキストです。__ 
`top_p`、`top_k`というのはテキスト生成時のパラメータで、いずれも __大きいほど生成結果の多様性を高める__ 効果があります。今回は記事全文を通して`top_p = 0.95`, `top_k=30`を使用しました。

### 入力テキスト
今回は、プロンプトとして与えるネタ先頭のフレーズを、3段階で検討しました。各段階について、どの程度 __「自然な日本語か」「ジャルジャルのネタっぽいか」「バリエーションが生じているか」「面白そうなタイトルになっているか」__ などを見るためです。

- ジャルジャル
    - 実在するネタに出てきがちなタイトルの冒頭を独断で選んだもの
    - ジャルジャル好きの方は、特定のネタが思い浮かぶかも
- 一般
    - ジャルジャルのネタタイトルには特に登場しないが、ごく一般的な語彙
    - [ランダム単語ガチャ](https://tango-gacha.com/)というサイトで引いた単語を使用（名詞の場合はランダムに助詞を挿入した）
- 専門
    - 「qiita」「機械学習」など、やや専門的な語

各群につき10個ずつのプロンプトを考え、プロンプト1種類につき30回の生成を行いました。

|ジャルジャル|一般|専門|
|-|-|-|
|就職面接で|アイシャドウは|qiitaで
面接官を|嬉しいと|エンジニアやのに
角刈りのくせに|常夏で|機械学習は
ヤバい|パワーストーンが|aiで
オーディションで|復活|chatgptに
漫才中に|イタリアンは|数学が
バーで|仲間も|pythonを
友達が|逃げる|プルリク
軽い知り合いに|5億円を|githubの
変な|アクシデントと|エラーが出たら

# 結果

ファインチューニングは行いましたが、特に出力に制約を加えたわけではないので、本家タイトルのように必ず「奴」で終わるテキストが生成されるわけではありません。

プロンプトごとに、生成30回のうちいくつ「奴」で終わったかをカウントしてみました。

![yatsu_count.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2703929/4730e874-c717-e1c2-bf6e-55593bfba9c0.png)

"ジャルジャル"群については、いい感じに生成が行われていることがわかります。他の2群では、プロンプトごとにまちまちといった所でしょうか。一部のプロンプトでは、学習データにない語彙でもネタのタイトルを生成できていそうです。

いくつか生成例も見てみます。

### prompt: 面接官を
```
- 面接官をにっこりさせてハズいキャラを演出する奴
- 面接官をこちょこちょしてごまかす奴
- 面接官をフグでおちょくりまくる奴
```
ヤバい奴感がよく出てる。ジャルジャルのコントっぽいです。
「フグでおちょくりまくる」って何？？？

### prompt: バーで
```
- バーで客に「お前ちゃうねん!」って言われる奴
- バーで隣の家のオッサンと口論になって、殴る奴
- バーでおじいさんにチューしてるとこ見られる奴
- バーで客に「なんでミスするの?」って言う奴
```
ところどころ本家のタイトルを思わせるフレーズが見受けられます。
なかなかシュールなコントが想像できます。

### prompt: オーディションで
```
- オーディションで泣き虫キャラを指摘され、そのまま不合格にする奴
- オーディションで800人の中から選ばれし者 主役の座を懸けた最後のバトルロイヤルに挑む奴
- オーディションでウケ過ぎて、坊主するハズレの芸人な奴
- オーディションで歌声で100点とったのに、落とされたことある奴
```
比較的長めのタイトルでもいい感じに生成してくれています。
どれも状況がシュールで、YouTubeのおすすめに出てきたらクリックしてしまいそうです。

### prompt: エンジニアやのに
```
- エンジニアやのになんでミスするの?」って言う奴
- エンジニアやのになんで怒られてるのかわからん奴
- エンジニアやのになんでお前ら英語できないの?」って言われる奴
- エンジニアやのになんで笑ってるの?」って言われる奴
- エンジニアやのになんで日本語わからないの?」って言う奴
```
GPT-2さん、急に刺さないでください。
エンジニアだってミスしますし、英語も日本語もわからん時はあります。

### prompt: aiで
```
- aiで誰にでもわかる簡単なクイズ』を企画した奴
- aiでゃあなるわ」とつぶやいた後、自首する奴
- aiであっ、ダメな奴って言われる奴
- aiで誰にも負けへん奴
```
『AIで誰にも負けへん奴』←早くこれになりたい。

### prompt: その他
```
- 就職面接で逆質問されて頭抱える奴
- 就職面接で「将来は社長になるのが夢です」って、はっきり言う奴
- 角刈りのくせに関係の濃い奴
- 角刈りのくせに 大学の入学式でガチガチに固められた奴
- 軽い知り合いに 2ch やってると思われる奴
- 漫才中ににこやかに... してるけどヤバい奴
- ヤバい先生やめて声優やらされてる奴
- 嬉しいとめっちゃくちゃ喜びまくる奴
- パワーストーンがありますよー!!って言ってくれる友達を大事にする奴
- アクシデントと言っておくけど、ちょっと変な奴
- プルリクはじけてる奴
- 数学が3つ”できる”奴
- エラーが出たらyes とだけ言う奴
- エラーが出たら1回だけリセットするボタンがあるから、それ押してまた同じことする奴
- qiitaではてなブックマークしてるの見せられる奴
- 機械学習は手品やダンスのイントロダクションの応用でしょぼい奴
- chatgptに対処はやい奴
```
専門的なプロンプトに対しても、いい感じに生成してくれる（ときもあります）
気に入ったものがあればコメントいただけると幸いです。

### 失敗例
```
- 就職面接で役に立ちました!- インタビューの答え方 _ 就活面接で役に立つ 話の受け方 _ 就活面接の自己アピール
- 面接官をにゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ にゃんこ
- バーで居眠り)
- 軽い知り合いに』(軽い知り合いに)は、日本の歌手、平井堅の2ndシングル。 自身の2作目のシングル。 オリコンシングルチャート1位を獲得した。 軽い知り合いに(4:13) 作詞・作曲:森雪之丞 / 編曲:亀田誠治・亀田誠治・亀田誠治
- アイシャドウはアイブロウパウダー使用 チークは口紅リキッドタイプ、チークは口紅リキッドタイプ、リップクリームは口紅リキッドタイプ使用 チークは口紅リキッドタイプ、リップクリームは口紅リキッドタイプ使用 口紅リキッド
- 常夏で作詞:秋元康、作曲:後藤次利、編曲:船山基紀 男なんかいらない!? パート1 女なんかいらない!? パート2
- アクシデントと 対処と 対処な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な人な
- qiitaではてなブックマークのコメントをはてなブックマークに反映させる方法
- qiitaでいいね! 、コメント とってもうれしいです。
- 機械学習はニューラルネットワーク ニューラルネットワーク上で学習する ニューラルネットワークは、認知と学習に効果的で、学習によって自然に言語を覚える能力を持った言語の仕組みを模倣する言語の仕組みである。 ニューラルネットワークは、機械学習を特徴として、認知能力を発達させる
- 数学がa 型の代数系で正則となるようなもので、次のうちどれか。 (a • •) が正数となるようなもので、次のうちどれか。 30° • 0° • 30° • 0°
- pythonを///////// \,///////// \,////// \,///////// \,/////// \
- プルリク声 - 松井範雄 24歳の青年。 バイト先の先輩に恋をしてしまったが、後輩に先を越されてしまった。 本人も気づいていない。 バイト先で知り合ってから、ずっとお互いに想い合っていた。 先輩に背中を押されて、思い
- githubのissue: issue 4 alphabet publishing publishers
```

一度ループに陥ってしまうと戻ってこれない感じがあります。

また、あまりにもプロンプトが訓練データ（ネタのタイトル）とかけ離れていると、ネタのタイトルっぽくないテキストが生成されてしまうようです（当然の結果とも言えますが・・・）

# まとめ

本記事ではGPT-2をファインチューニングしてジャルジャルのネタタイトルの生成を行いました。

プロンプトの与え方次第なところはありますが、いい感じに「ジャルジャルのネタっぽい」テキストを生成できていると言えるのではないでしょうか。

本件に取り組んでみて感じたのは、APIやライブラリを駆使すれば、__目的に応じた生成モデルをごく簡単に作ることができる__ ということです。データを用意できれば、`transformers`にあるスクリプトを使用することで、自然言語処理やディープラーニングの知識がほとんどなくても、自分だけの生成AIを作れてしまいます。良い時代になったものですね。