PodcastをWhisperで文字起こしして、BERTで句読点抜きの文章に句読点を付与する（その２）

# はじめに

[前回の記事](https://qiita.com/SoySoySoyB/items/14334bc8af1bd7915d44)では、Podcast配信の仕組みを踏まえて、各エピソードの音声ファイルのURLを取得し、Whisperで文字起こしするところまでを書きました。このとき、文字起こしはできたものの、なぜか句読点が入らないという問題がありました。

そこで今回は、句読点のない文章に句読点を入れる方法を取り上げます。先にお伝えしておきますが、私はこの領域には知見が浅く、[「句読点の無い文章に句読点を挿入する（BERTによる予測）」](https://qiita.com/sentencebird/items/a916cc8d874470c3b2fc)の記事を大いに参考にさせていただきました。


# こんな方におすすめ

- 文字起こしをしたものの句読点が入らず困っている
- あまり詳しくないもののとりあえずBERTを動かしてみたい


# 句読点付与の成果

先にどのような結果となったのかをお見せしましょう。正直精度が微妙なのですが、その点に関する考察は後述します。

> 【入力文章】
始めるを応援するポッドキャスト スタートFMおはようございます起業家でラジオパーソナリティー の関口舞ですテーラー株式会社の柴田陽さんと起業や独立を考えている 方に役に立つ情報を楽しく語っていきます 陽さんおはようございます陽さん おはようございますなんかいつもよりニコニコしてません どうしましたえ 表情が声に乗っかるって言う じゃないですか確かにそれは本当にそうですよね だからコールセンターとかですね自分の前に鏡置いて口角がちゃんと 上がっているようにしましょうみたいになるんでちょっと無理に 作り笑いをしている珍しい 伝わりましたでしょうか伝わってね皆さんこのニコニコ した明るい陽さんの声が伝わりましたでしょうかなんか最近アメリカの会社とちょっと 仕事をしていましてどういう意味ですか法人がアメリカにあって生成AI のスタートアップみたいなところちょっとやっているんですけど リモートで世界中の人が社員として参画している的な企業と今やり取り していて（以下略）

> 【出力文章】
始めるを応援するポッドキャストスタートFMおはようございます起業家でラジオパーソナリティーの関口舞ですテーラー株式会社の柴田陽さんと、起業や、独立を考えている方に、役に立つ情報を、楽しく、語っていきます。陽さんおはようございます陽さんおはようございますなんか、いつもよりニコニコしてませんどうしましたえ表情が声に乗っかるって言うじゃないですか。確かに、それは、本当にそうですよね。だから、コールセンターとかですね自分の前に鏡置いて、口角が、ちゃんと上がっているようにしましょうみたいに、なるんで、ちょっと、無理に作り笑いをしている珍しい伝わりましたでしょうか伝わってね皆さんこのニコニコした、明るい陽さんの声が、伝わりましたでしょうかなんか最近、アメリカの会社と、ちょっと仕事をしていまして、どういう意味ですか。法人がアメリカにあって、生成AIのスタートアップみたいなところちょっとやっているんですけど、リモートで世界中の人が社員として参画している的な企業と今やり取りしていて、（以下略）


## Pythonコード

これを実行するためのコードは以下の通りです。なお、ライブラリの依存関係が面倒という記述を見かけたので、Google Colaboratoryで実行しています。

```python
pip install transformers fugashi unidic-lite

from transformers import pipeline
nlp_base = pipeline('fill-mask', model='cl-tohoku/bert-base-japanese-char-v3')
nlp_large = pipeline('fill-mask', model='cl-tohoku/bert-large-japanese-char-v2')

def insert_char_to_text(i, char, text):
    l = list(text)
    l.insert(i, char)
    inserted_text = ''.join(l)
    return inserted_text

def text_correction(text, thresh, model, chars_count):
    text = text.replace(' ', '')  # なぜか元の文章に半角スペースが入っていたので除外
    nlp = model
    punctuations = ['、', '。', '?', '？']

    i = 0
    corrected_text = text
    while i < len(corrected_text):
        i += 1
        if corrected_text[i-1] in punctuations: continue

        masked_text = insert_char_to_text(i, nlp.tokenizer.mask_token, corrected_text)
        pre_text = masked_text.split("。")[-1].split(nlp.tokenizer.mask_token)[0][-chars_count:]
        post_text = masked_text.split(nlp.tokenizer.mask_token)[1][:chars_count]
        res = nlp(f'{pre_text}{nlp.tokenizer.mask_token}{post_text}')[0]

        if res['token_str'] not in punctuations: continue
        if res['score'] < thresh: continue

        punctuation = res['token_str'] if res['token_str'] not in ['?', '？'] else '。'
        corrected_text = insert_char_to_text(i, punctuation, corrected_text)
    return corrected_text

text = 'ここに句読点を入れたいテキストを入れる'  # コードを書くのが面倒だったのでファイルからの読み込みはしなかった
text_correction(text, 0.6, nlp_base, 50)
```


## コードの解説

基本的には参考にした記事のコードを流用していますが、そのまま実行するとエラーを起こす部分があったため、内容は調整しながら活用しています。行っている処理としては、下記のような内容になっています。

- 文章のi番目にマスキングされた文字があると仮定したときの、その文字種別の予測を行う
- i-1番目が句読点の場合、句読点は連続しないのでそのままにする
- それ以外の場合、一番確率が高いと算出される文字種別とその確率を計算し、それが句読点かつその確率が閾値以上のときに、その場所に句読点を追記する

参考記事との違いについては、そのまま使うと上記のpre_text部分がトークン上限を超えるため、コードを調整しています。元コードでは「。」で分割をするのみですが、しばらく文章上で句点が存在しないようなものだと、入力トークンの上限を超えてしまうのが要因です。


## 利用している言語モデル

[東北大学の自然言語処理研究グループ](https://www.nlp.ecei.tohoku.ac.jp/research/open-resources/)が[Hugging Faceに公開](https://huggingface.co/cl-tohoku)している、日本語BERT訓練済みモデルを利用しています。概要は下記のように説明されています。

> 日本語版Wikipediaをコーパスに用いて訓練した、汎用言語モデルBERTの訓練済みモデルです。MeCab(ipadic)とWordPieceを用いて単語分割したモデルと、文字単位で単語分割したモデルの2種類を公開しています。これらのモデルは、Hugging Faceによる自然言語処理ライブラリTransformersでも訓練済みモデルとして利用可能です。

機械学習に詳しい同僚に聞いたところ、東北大学の言語モデルは、言語処理系では非常によく使われているとのことでした。


## パラメータの調整

コードとしてはシンプルですが、処理としては非常に重いものであったため、いくつかパラメータを調整しながら実行しています。

- モデル：baseとlargeでは精度的にはそれほど変わらなかったものの、largeだと処理時間が飛躍的に伸びるため、baseモデルを活用
- 閾値の確率：0.5では句読点がやや少なく、0.7では無駄な場所に入ってしまうようだったので、0.6と設定
- 前後の文字数：マスキング部分は前後の文章をもとに確率計算をしており、その文章の長短に応じて処理時間が変わるが、100では時間がかかりすぎ、25ではやや精度が悪化するため50で設定


# その１と合わせた考察

まず、実行環境とパフォーマンスについてまとめておきます。

- Whisperによる文字起こし
    - 過去に実行環境を構築していたため、ローカルPCで実施
    - 第11世代i7、メモリ16GBのLet's note
    - 27分程度のエピソードに対して、largeモデルで1時間弱の処理時間
- BERTによる句読点付与
    - 無料版のGoogle Colaboratory
    - 文字起こしの全文に対して、baseモデル、前後文の閾値50文字で1時間程度の処理時間

つまり、30分弱のエピソードに対して、データ処理に2時間も時間がかかってしまいました。


上記を踏まえた上で、今回の検証を進めながら考えたことを書き出しておきます。

- そもそもの題材の難しさ
    - 今回使用した音声は、口語でかなり話者交代が多いので、文字起こしは難しい部類に思える（話者が1人のエピソードではもう少し精度が上がるかもしれない）
- Whisperの処理
    - 軽く検索するだけでもFine-Tuningをする記事は多く出てくるので、適切に調整をすれば精度は上がると思われる
    - 句読点の有無を分ける条件についてはよくわからない
- BERTによる処理
    - 今回利用している東北大学のモデルは、元にしているデータがWikipediaであるため、口語に適用するには適切ではないのではないか
- 共通
    - 自分が知らないだけで工夫の余地は多くあるのだろうとは思うが、やはり自然言語処理はマシンパワーが必要なので、本気でやろうとするならGPUを利用するのが順当そう


# さいごに

実用レベルでは全くないのは残念ですが、今回少しいじってみて自然言語処理についていい勉強になりました。まだまだこの領域にはひよっこもいいところなので、間違いやご助言などあればぜひいただけると嬉しいです。