100年前の教科書をNDLOCR+fugashiで形態素解析

# Introduction

先日大学院で戦前用いられていた道徳（修身）教科書についての研究をしている友人から、教科書の形態素分析を手伝ってほしいという依頼がありました。教科書の特徴としては下記が挙げられます。
-  手持ちの教科書はデジタルデータ（PDF）であり文章は画像として保存されている
- 教科書に書かれている日本語は大正〜昭和時代のものであり旧字体や旧仮名が使われている
下記の画像が実際の教科書のページの一例です。

![形態素解析を実施した教科書のページ例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031575/d12bf607-03dc-f8d4-5cd5-5a90ada39c7f.png)

なかなかに小難しい漢字が並んでいますね、、
そのため、形態素分析をする上では

:::note 
**分析要件**
1. 縦書、かつ旧字体の織り混ざっている日本語を識別できるOCRを利用する必要がある
1. 抽出した文章に対して、大正時代のコーパスに対応した分析を実施する必要がある
:::

が要件となってきます。

**本稿では上記二つの点についてどのようにPythonで実装したか話したいと思います。**

# OCRモデル

## Tesseract

さてまずは画像をテキスト化する作業からです。これは一般的にOCR (Optical Character Recognition) と呼ばれる分野ですが、最近は手持ちのスマホをかざすだけでカメラに映った文章をテキスト化できることから分かる通り、ある程度成熟した分野となっています。そのためクイックに実装可能な手法がいくつかあるのですが、今回はGoogleが公開しているLSTMをベースにした[Tesseract](https://github.com/tesseract-ocr/tesseract)を用いようと思いました。

今回はPDFを扱うのでPythonのPDF用パッケージPyMuPDFと、上記TesseractのPythonラッパーである[Pytesseract](https://github.com/madmaze/pytesseract)を入れます。

```bash
pip install pymupdf pytesseract
```

Pytessearctの利用方法は比較的シンプルで下記のコードで画像のテキスト抽出が可能です。

```python
import fitz  # PyMuPDF
import pytesseract
from PIL import Image

pdf_path = "hoge/hoge.pdf"

# PDFファイルを開く
pdf_document = fitz.open(pdf_path)
extracted_texts = []

for page_num in range(len(pdf_document)):
	# PDFの単体ページを取得
	page = pdf_document.load_page(page_num)
	
	# ページを画像に変換
	pix = page.get_pixmap()
	image_bytes = pix.tobytes("png")
	image = Image.open(io.BytesIO(image_bytes))
	
	# 画像をTesseractに読み込ませる
	text = pytesseract.image_to_string(image, lang="jpn_vert")
	extracted_texts.append(text)
	pdf_document.close()
```

今回は縦がきの日本語の抽出を実施したいため、pytesseractの言語を  `jpn_vert` (japanese vertical) に設定しています。なお縦書き日本語のモデルはpytesseractインストール時にデフォルトでダウンロードされるため、out of the boxで利用できます。クイックに実装ができて非常に便利ですね。

さて下記がPytesseractを実行した結果です。

> 幣容世の中に出て大きな仕事をなし、往會の中堅となるべき愛等青年は何事のカカをからちらないてで、自分自身で5ならね。修學に於てもなる<べく先生や友人の力をからザず出森るだけ自ら考(へ自ら解くやうに我が心を働かすならば、これは卸ち自學自修である我等が中源校に入史したのは、陸に物事を多く記憶する-ためではない。我が心を働かがかせて行く力と自ら調べる心とを養ふためである。この力とこの心を有つてみる者は、忠に費力ある人となって人何護までも向上北展するものである

うーむ、、内容は分かるし結果は悪くないのですが、一番最初の「將來」含めて明らかに旧字体を外しています。

ちなみにこの状態でChatGPTに誤字を直してくれるようにお願いすると下記の通りになります。

> 幣容世の中に出て大きな仕事をなし、**社會**の中堅となるべき**我等**青年は何事もかかをからちらないてで、自分自身で**成さね**ばならね。**學業**に於てもなるべく先生や友人の力を借りず、**出來る**だけ自ら考へ自ら解くやうに我が心を働かすならば、これは**實に**自學自修である。我等が中學校に**入學**したのは、**単に**物事を多く記憶するためではない。我が心を**働かせて學ぶ**力と自ら調べる心とを養ふためである。この力とこの心を有つてみる者は、**實**に有力ある人となって**何事**も向上し**展開**するものである。

明らかにおかしな誤字を直していますがやはり冒頭の「將來」含めておかしな点は残ります。また本文と比較するといくらか憶測が入ってしまっています。

:::note info
なお今回はプロンプトにて「オリジナルをなるべく残す」と指示していますが、もし分析の目的が教科書の内容を理解・要約する等であれば、現状でも文脈を十分に理解できているので以上の解析で事足りるかと思います。
**いかんせん今回は形態素分析を実施し各単語のカウントなどを実施したいので、単語が消えたり新たに出てきたりするのは避けたい所存です、、**
:::

## NDLOCR

さて上記の結果から見るに、どうやらTesseractの日本語モデルは旧字体に対応していないように思われます。そこで考えられる対処手段としては
-  他のモデルがないか探す
- なければ、tesseractの追加学習を実施
	- その場合旧字体を含む学習データセットを見つける必要がある

があります。
今回は運良く国立国会図書館が蔵書をデジタル化する上で開発したテキスト抽出モデル[NDLOCR](https://github.com/ndl-lab/ndlocr_cli/)が公開されていたため、こちらを利用する方針としました。
細かい仕様までは確認していませんが、ソースコードを確認したところ、下記の流れでテキスト抽出を実施していると思われます

1. 見開きページを分割
2. 傾きを補正
3. レイアウトを認識
4. 各行の文字を認識

こちらDockerコンテナが提供されておりローカルで回せるのですが、僕の自前PCのスペックが非常に低くGPU推論ができないため、同じく提供されている[Google Colabのノートブック]()を利用しました。
公開ノートブックは[非常にフレンドリー](https://zenn.dev/nakamura196/articles/43151b473e8954)であり、基本的に画像あるいはPDFのURLを渡すだけで解析を実施しテキストファイルを渡してくれる仕様となっています。
一方で僕が試したところ、画像ファイルの受け渡しが画像サイズ等によってはうまく行かなかったり、PDFもそのまま渡すとColabのGPUリソースだとエラーが出たりしました。
いくつか試行錯誤を繰り返した結果、単一のPDFページであれば問題なく解析が回ることを確認したので、オリジナルのPDFを単一ページの複数PDFに分割することで対応しました。

さてNDLOCRを回して出てきた結果はこちらとなります。

>自學・自修の利盆
>將來世の中に出て大きな仕事をなし、社會の中堅となるべき我等青年は、何事も他人の力をからないで、自分自身で成し遂げようとする心がなければならぬ。修學に於ても(なるべく先生や友人の力をからず、出來るだけ自ら考へ自ら解くやうに我が心を働かすならば、これは即ち自學・自修である。我等が中學校に入學したのは、單に物事を多く記憶するためではない。我が心を働かせて行く力と、自ら調べる心とを養ふためである。この力とこの心を有つてゐる者は、眞に實力ある人となつて、何處までも向上・發展するものである。

**いい感じに旧字体も認識しているだけでなく、ページ上部の目次など、レイアウトの特徴も捉えていますね！本文と比較しても圧倒的な精度、、** さすがは国立図書館御用達のモデルです。

# 形態素解析

続いて形態素解析ですが、こちらは端的にいうとテキストデータに対して各単語を抽出し、その単語の性質（名詞なのか、動詞なのか、動詞であれば活用は何か、など）を記述するというものです。詳しくは[Wikipedia](https://ja.wikipedia.org/wiki/形態素解析)などを参考ください。
## fugashi + unidic

日本語の形態素解析の有名どころといえば、言わずと知れた[mecab](https://aizine.ai/mecab-0327/)ですが、今回はより軽量かつユーザーフレンドリーな[fugashi](https://github.com/polm/fugashi)を利用したいと思います。FugashiそのものはmecabのCythonラッパーであり、分析を実施する上では別途辞書（日本語の単語帳のようなもの）をダウンロードする必要があります。

```bash
pip install fugashi
pip install unidic  # Unidic形式の辞書を利用するためのライブラリ
python -m unidic download  # 最新の現代語Unidic辞書をダウンロード
```

これでfugshiの利用が可能となります。Mecabは別途ダウンロードする必要はなく、fugashiとともに勝手にダウンロードされます。
Fugashiの利用は非常にシンプルで、あっという間に名詞の抽出が可能となります。なおfugashiのトークンは `feature`  と呼ばれるプロパティを持っており、こちらに単語の[品詞](https://ja.wikipedia.org/wiki/品詞)（Part Of Speech）が含まれています。

```python
from fugashi import Tagger
from collections import Counter

file_path = "hoge/hoge.txt"  # OCRで抽出された文書
exclude_words =  ["こと", "もの"]  # 除外したい名詞

# Fugashiのトークナイザーを初期化
tagger = Tagger()

# OCR解析で得た文章を読み込み
with open(file_path, "r", encoding="utf-8") as file:
	text = file.read()

# 普通名詞を抽出。feature[0]が主品詞、feature[1]が副品詞
words = [word.surface for word in tagger(text) if word.feature[0] == "名詞" and　word.feature[1] == "普通名詞"]

# いくつかの名詞を除外
words = [w for w in words if w not in exclude_words]

# 文書中における各名詞の利用数をカウント
word_counts = Counter(words)
```

上記のスクリプトをもとに抽出した上位10個の名詞が下記です。

```python
[('人', 63), ('會', 42), ('課', 38), ('社', 37), ('精神', 33), ('事', 31), ('身', 29), ('心', 28), ('警察', 26), ('自分', 25)]
```

悪くない、、悪くない、が。
実はこちらだと先ほどのOCRに類似した問題が発生します。というのも、デフォルトのUnidic辞書だと現代の書き言葉のみに対応しており、**旧字体や旧仮名を適切に識別していません。**

:::note warn
冒頭のページに対して実施した下記の中間出力を見れば明白かと思いますが、例えば「實力」（実力）を「實」と「力」二つに分けてしまい、尚且つ前者は人名と判別してします。「眞」（真）も同等です。
:::

![スクリーンショット 2023-11-25 14.42.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031575/bc45ef82-cc0c-cea6-3094-4a63fbe22821.png)

## fugashi + unidic-novel

さてでは旧字体、ないし大正〜明治時代の日本語に対応するにはどうすればいいでしょうか。
実はUnidic辞書を公開している国立国語研究所は、[古文向けのUnidic辞書](https://clrd.ninjal.ac.jp/unidic/download_all.html#unidic_chj)も提供しています。今回の用途としてはその中でも明治〜現代の小説に対応した近現代口語Unidicを利用したいと思います（近代文語を使う、という手段もあり得たのですが、依頼した友人曰く1920年以降の日本の教科書は文語から口語文に統一されたらしいです）。

先ほどの[サイト](https://clrd.ninjal.ac.jp/unidic/download_all.html#unidic_chj)で辞書をダウンロードしたら、あとはFugashiを利用する際に辞書の格納されているパスを指定するのみです。一例としてダウンロードした辞書が `/Users/hoge/my_dictionaries/unidic-novel` にあるとすると、前述のスニペットのうちトークナイザーを初期化しているところを下記のように対応する形です。

```python
mydict_path = "/Users/hoge/my_dictionaries/unidic-novel"
tagger = Tagger(f"-d {mydict_path}")
```

これでfugashiが近現代口語の辞書を使えるようになります。
上記辞書を設定した上で改めて最頻単語を分析した結果がこちらです。

```python
[('人', 66), ('課', 38), ('社會', 37), ('精神', 33), ('心', 29), ('事', 28), ('警察', 26), ('自分', 25), ('時', 23), ('宗教', 22)]
```

先ほどは単体で出てしまっていた「社」「會」が今回は「社會」と一つの単語で出てきていることを確認できました！ランキングも微妙に変わっていますね。
より詳細に見てみると先ほどは誤認識されていた「實力」や「眞」が適切な形態素をとっていることもわかります。

![スクリーンショット 2023-11-25 14.48.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031575/1948c448-e391-2fb4-70e8-00a419ced67c.png)

# まとめ

この記事では日本の蔵書に特化したOCRモデルであるNDLOCRと、fugashi+近現代口語Unidicを活用することで、100年近く前に書かれた教科書の形態素解析を実施しました。
今回はNDLOCRモデルの性能が非常に高くそのまま利用するにとどまったため、余裕のある時に別途tesseractの追加学習も試して比較してみたいと思います。