コンテンツへスキップ
チュートリアル19分で読めます

Transkribus と eScriptorium の HTR 出力を行ごとに修正する

Potato で二人の転写者が各 HTR 行をページ画像と照合し、再学習用に PAGE XML をエクスポートして、モデルの文字誤り率を計算します。

Potato Team

Transkribus または eScriptorium の手書き文字認識(HTR)出力を持つ文書館職員、歴史学者、編集者は、このチュートリアルに従って、Potato 2.13.1 で二人の転写者に認識されたすべての行を修正させ、修正を再学習用に PAGE XML へ書き戻し、修正からモデルの文字誤り率を計算できます。PAGE XML は、Transkribus、eScriptorium、Kraken エンジンが、一枚のページ画像の領域、テキスト行、認識されたテキストを保存する XML 形式です。Potato はテキスト行ごとに一つの項目を作り、ページ画像から行を切り出して、認識されたテキストを切り出しの下の編集可能なボックスに表示します。実例には、すべての行に正解の転写がある 17 世紀の市参事会議事録の四ページを使います。そのため、このページの誤り率は正解キーと照合できます。デジタル人文学の形式はすべてのインポートとエクスポートのオプションを説明しており、このページでは、PAGE ファイルから修正済みのファイルまで、一つの修正プロジェクトを追います。

ページと認識されたテキスト

例のページは、ボーツェン(ボルツァーノ)の Ratsprotokolle、すなわち 1470 年から 1805 年までの市参事会の会議録のものです。議事録は初期近代ドイツ語で手書きされています。READ プロジェクトは、そのうち 450 ページと、各行の正解の転写を PAGE XML で、HTR Dataset ICFHR 2016 として CC BY 4.0 の下で公開しました。テストセットは Test-ICFHR-2016.tgz(59 MB)で、50 枚のページ画像と、画像ごとに一つの PAGE ファイルを収めた page/ フォルダーがあります。Potato のインポーターは、Transkribus のエクスポートにこのフォルダー構成を想定しています。このチュートリアルでは 405 から 408 ページを使います。四ページには 88 のテキスト行と、1,732 文字の正解があります。

データセットの PAGE ファイルには、モデルの読みではなく正解が入っています。そこで Potato チームは、修正対象のテキストを作るため、データセット自身の行の境界に対して HTR モデルを実行し、Transkribus は使いませんでした。モデルは、Kraken 用の Stefan Weil によるドイツ語手書きモデルで、Weil はこのモデルを CC BY-SA 4.0 で公開しています。Kraken は eScriptorium の内部にある認識エンジンです。Kraken は XML ファイルの隣にあるページ画像を探すため、まず page/ から四つの PAGE ファイルをコピーします。

bash
pip install kraken==7.1.1
cd Test-ICFHR-2016
mkdir -p ../htr/page
for n in 0405 0406 0407 0408; do
  cp page/Seite$n.xml .
  kraken -f page -x -i Seite$n.xml ../htr/page/Seite$n.xml ocr -m ../german_handwriting.mlmodel
  cp Seite$n.JPG ../htr/
done

-f page は、行を再検出する代わりに PAGE ファイルから行のポリゴンを読み込み、-x は PAGE XML を書き出します。その結果、htr/ フォルダーは Transkribus の構成になり、画像が最上位に、PAGE ファイルが htr/page/ に置かれます。Kraken は各行の信頼度を書き込み、88 行のうち 52 行で信頼度が 0.95 以上でした。正解に対して、Kraken のテキストの文字誤り率は、50 ページすべてのテストページで 8.4% でした。モデルの学習データには、データセットの 400 の学習ページが含まれており、50 のテストページは学習ページに含まれません。したがって Kraken はこのコレクションの筆跡を見たことがあり、モデルにとって新しいコレクションでは、より高い誤り率になることがあります。ご自身の Transkribus または eScriptorium のエクスポートがある場合は、htr/ の代わりにそのエクスポートを使って、次のステップから始めてください。

PAGE XML の Potato へのインポート

Potato をインストールし、フォルダーをインポートします。

bash
pip install potato-annotation==2.13.1
potato import -f page_xml -i htr/ -o htr-correction/
text
Imported 88 item(s), 1 question(s), 0 answer(s) from 0 annotator(s), and 0 model suggestion(s) from PAGE XML.
  wrote htr-correction/data/htr.jsonl
  wrote htr-correction/page_xml/page/Seite0405.xml
  wrote htr-correction/media/Seite0405.JPG
  ...
  wrote htr-correction/config.yaml
  wrote htr-correction/README.md

-f transkribus も同じインポーターを指します。インポートはフォルダー内の PAGE ファイルを検索し、各ページの画像を XML の隣と一つ上のフォルダーで探して、画像を media/ にコピーします。別の場所に保存された画像には、フォルダーのパスを指定して --image-dir を追加します。PAGE ファイルの imageFilename が IIIF 画像 URL のようなウェブアドレスである場合、項目はそのアドレスを保持し、ブラウザーがそのサーバーから画像を読み込みます。インポートは各 PAGE ファイルのコピーを page_xml/ の下にも保持し、エクスポートは後でそれらのコピーに修正を書き込みます。

各行は、ページの読み順で data/htr.jsonl の一つの項目になりました。405 ページの二行目が、そのフィールドを示します。

json
{"id": "Seite0405-line_b0d3dab2-88ff-4574-9081-768f21788681",
 "htr_text": "schofft. Aūch das Er der",
 "line_image": {"image": "/media/Seite0405.JPG", "width": 2322, "height": 3511,
                "region": [700.0, 500.0, 1055.0, 97.0],
                "polygon": [[1755.0, 507.0], [1405.0, 500.0], [700.0, 508.0], [700.0, 597.0], [1405.0, 589.0], [1755.0, 596.0]],
                "baseline": [[700.0, 568.0], [1405.0, 560.0], [1755.0, 567.0]]},
 "page": "Seite0405.xml", "page_xml": "page_xml/page/Seite0405.xml",
 "region_id": "region_2a649214-6882-4084-978c-59e74c45047f",
 "line_id": "line_b0d3dab2-88ff-4574-9081-768f21788681",
 "line_number": 2, "htr_confidence": "0.9855"}

htr_text は Kraken の読み、line_image はページ上の行の位置、htr_confidence はその行についての Kraken の信頼度です。line_id は、項目を PAGE ファイルの TextLine 要素に結び付けます。領域や行そのものを修正するには、--page-xml-mode regions が、各ページを、すべての領域と行をポリゴンとして持つ一つの項目としてインポートします。

修正用の設定

インポートは、一つの text_edit スキームと image_region ディスプレイを持つ、動作する設定を書き出します。下の設定は、インポートされた設定に五つの変更を加えます。

  • タスク名が htr (imported) に置き換わります。
  • num_annotators_per_item: 2 は、すべての行を二人の転写者に送ります。
  • assignment_strategy: fixed_order は行を読み順に配信するため、転写者は書き手の順序でページの行を読みます。
  • 二つ目の表示フィールドが、その行についての Kraken の信頼度を表示します。
  • スキームの description が転写の規約を示します。異なる規約に従う二人の転写者は、同じ行について異なるテキストを作るためです。

この設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。

yaml
port: 8000
annotation_task_name: Ratsprotokolle line correction
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/htr.jsonl
item_properties:
  id_key: id
  text_key: htr_text
user_config:
  allow_all_users: true
  users: []
site_dir: default
assignment_strategy: fixed_order
num_annotators_per_item: 2
instance_display:
  fields:
  - key: line_image
    type: image_region
    label: Line
  - key: htr_confidence
    type: text
    label: Kraken confidence
annotation_schemes:
- annotation_type: text_edit
  name: transcription
  description: Correct the transcription of this line. Keep the scribe's spelling and abbreviations.
  source_field: htr_text
  show_diff: true
  show_edit_distance: true
  allow_reset: true

show_diff は Kraken のテキストに対する単語単位の差分を表示し、show_edit_distance は変更された単語と文字の数を表示し、allow_reset は Kraken のテキストを復元するボタンを追加します。実際のプロジェクトの description では、次のセクションの ū や ¬ のような特殊文字を一つずつ挙げるでしょう。

行の修正

プロジェクトのフォルダーからサーバーを起動し、各転写者にユーザー名とパスワードで登録してもらいます。

bash
cd htr-correction
potato start config.yaml -p 8000

ページには、ページ画像から切り出された行が、切り出しの上に行の輪郭を描いた状態で表示され、「Show the whole image」は、行に印を付けた全ページを開きます。切り出しと Kraken の信頼度の下で、「Original」は Kraken のテキストを表示し、「Edit below」の下のボックスは同じテキストから始まります。転写者が入力している間、「Words changed」と「Chars changed」が編集を数え、「Changes」は置き換えられた各単語に取り消し線を引き、古い単語の隣に新しい単語を表示します。差分は単語単位で働きます。Kraken は 405 ページの最初の行を 203 ではなく 263 と読み、差分は数字全体に取り消し線を引いて新しい数字を追加しますが、文字数の表示は一つの変更を示します。

すでに正しい行では、「Next」をクリックしてください。 Potato 2.13.1 は、変更のない行を編集距離 0 の承認済みとして保存するため、エクスポートでは、その行と誰も開いていない行とを区別できます。Potato 2.12.1 は、ボックスのテキストが変更されたか、リセットボタンが実行された後にのみ text_edit の回答を保存していました。2.12.1 では、手を付けなかった行は回答なしで保存され、進捗カウンターもその行を数えなかったため、そのバージョンでは「Next」の前に「Reset to Original」をクリックしてください。2.12.1 でのこのページのためのブラウザ確認では、「Next」だけで通過した行ではカウンターが 88 件中 1 のままで、次の行の「Reset to Original」でカウンターが 2 に進みました。

READ の転写は、スキームの description に書くべき二つの規約に従っています。一つは、書き手が u と n を区別するために u の上に書いた記号を表す ū、もう一つは、単語が分かれる行末の ¬ です。四ページの正解には、37 個の ū と 18 個の ¬ があります。

このページの裏付けとなったテスト実行

このページの数値は、正解を答えのキーとして使う二人のスクリプト化された転写者による、Potato 2.12.1 での実行によるものです。スクリプトは、アノテーションページが送るのと同じリクエストで各行を保存しました。最初の転写者 alice は、各行の正解をそのまま入力しました。二人目の転写者 bob は、ū をすべて通常の u にした正解を入力しました。Bob の規則は、正解とは異なる規約に従う転写者を表します。回答が Kraken のテキストと一致した行では、各転写者は「Reset to Original」が行を保存するのと同じ方法でその行を保存しました。Alice は 88 行のうち 56 行を変更し、Bob は 69 行を変更しました。Bob は、Kraken が正しく読んでいた行からも記号を取り除いたためです。実際の転写者は行を読み誤ることもありますが、スクリプトは読み誤りをしません。

修正済み PAGE XML のエクスポート

page_xml エクスポートは、各転写者の修正を、プロジェクトが保持する PAGE ファイルのコピーに書き込みます。

bash
python -m potato.export -c config.yaml -f page_xml -o export/
text
export/alice/page_xml/page/Seite0405.xml ... Seite0408.xml
export/bob/page_xml/page/Seite0405.xml ... Seite0408.xml

転写者が二人の場合、各転写者にフォルダーが作られ、エクスポートは 176 行が書き込まれ、125 行が変更されたと報告しました。--option annotator=alice は、ユーザーフォルダーなしで一人の転写者のバージョンを書き出します。保存された各行で、エクスポートはその行の TextEquiv のテキストを設定し、その行についての Kraken の信頼度を取り除きます。テキストが変更された行では、エクスポートは Kraken の Word 要素とその内部の Glyph 要素も取り除きます。古い単語と字形の位置は新しいテキストと一致しなくなり、--option keep_words=true を指定すれば要素は保持されます。エクスポートは LastChange の日付を更新し、各ファイルのそれ以外の内容は Kraken が書いたままにします。Alice の四つのファイルは、文字の差異なく正解と一致しました。

修正済みのファイルは、次のモデルの学習データになります。eScriptorium は PAGE XML の転写をインポートし、文書の転写からモデルを学習します。Kraken 自身のツールは、ページ画像がファイルの隣にあれば、それらのファイルを読み込み、ketos test は、修正済みのテキストに対するモデルの誤り率を報告します。

bash
cp media/*.JPG export/alice/page_xml/page/
ketos test -f page -m german_handwriting.mlmodel export/alice/page_xml/page/*.xml
text
=== report  ===
1732	Characters
151	Errors
91.28%	Character Accuracy

文字正解率 91.28% は、文字誤り率 8.72% です。同じファイルでモデルをファインチューニングできます。

bash
ketos train -f page -i german_handwriting.mlmodel --resize union -o finetuned export/alice/page_xml/page/*.xml

-i は既存のモデルから始め、--resize union は、修正が使う文字のうちモデルにないものを追加し、-o は新しいモデルのフォルダーを指定します。四ページでの 1 エポックの実行は、88 行を学習用 79 行と検証用 9 行に分け、モデルファイルをフォルダーに書き出しました。四ページは、より良いモデルのための十分なデータではなく、経路の確認にすぎないため、再学習の前に、はるかに多くのページから修正を集めてください。

修正前後の文字誤り率

文字誤り率(CER)は、モデルのテキストを修正済みのテキストに変えるために必要な文字の挿入、削除、置換の数を、修正済みのテキストの長さで割ったものです。CER は単語誤り率の文字単位の形で、編集の数はレーベンシュタイン距離です。Potato 2.12.1 は text_edit について転写者間の一致度を計算しないため、二人の転写者の間の CER が一致度の値の代わりになります。JSONL エクスポートには、各行についての各転写者のテキストがあります。

bash
python -m potato.export -c config.yaml -f jsonl -o export/

各レコードは、行の回答を、edited_text、original_text、edit_distance_chars、edit_distance_words を持つ JSON 文字列として保存します。下のスクリプトは、Kraken のテキストを各転写者のテキストと比較し、二人の転写者を比較し、Kraken の誤りを Kraken の信頼度で分けます。スクリプトは htr-correction/ で実行してください。

python
import collections
import json
 
 
def distance(a, b):
    """Levenshtein distance between two strings, in characters."""
    previous = list(range(len(b) + 1))
    for i, char_a in enumerate(a, 1):
        current = [i]
        for j, char_b in enumerate(b, 1):
            current.append(min(previous[j] + 1, current[j - 1] + 1,
                               previous[j - 1] + (char_a != char_b)))
        previous = current
    return previous[-1]
 
 
def cer(pairs):
    """Character error rate of the first text in each pair against the second."""
    errors = sum(distance(hyp, ref) for hyp, ref in pairs)
    return errors / sum(len(ref) for _, ref in pairs), errors
 
 
# The HTR text and Kraken's confidence for every line
lines = {}
for row in open("data/htr.jsonl"):
    item = json.loads(row)
    lines[item["id"]] = item
 
# Each transcriber's corrected text for each line
corrected = collections.defaultdict(dict)
for row in open("export/annotations.jsonl"):
    record = json.loads(row)
    blob = record["labels"].get("transcription", {}).get("transcription")
    if blob:
        corrected[record["user_id"]][record["instance_id"]] = json.loads(blob)["edited_text"]
 
for user, texts in sorted(corrected.items()):
    rate, errors = cer([(lines[i]["htr_text"], texts[i]) for i in texts])
    print(f"HTR against {user}: CER {rate:.3f} ({errors} edits, {len(texts)} lines)")
 
first, second = sorted(corrected)
shared = sorted(set(corrected[first]) & set(corrected[second]))
rate, errors = cer([(corrected[second][i], corrected[first][i]) for i in shared])
differ = [i for i in shared if corrected[first][i] != corrected[second][i]]
print(f"{second} against {first}: CER {rate:.3f} ({errors} edits, {len(differ)} of {len(shared)} lines differ)")
 
# How wrong the HTR text was at each confidence level, against the first transcriber
bands = collections.defaultdict(list)
for i in shared:
    confidence = float(lines[i].get("htr_confidence", 0))
    band = "0.95 and up" if confidence >= 0.95 else "0.90 to 0.95" if confidence >= 0.9 else "below 0.90"
    bands[band].append((lines[i]["htr_text"], corrected[first][i]))
for band in ("below 0.90", "0.90 to 0.95", "0.95 and up"):
    rate, errors = cer(bands[band])
    print(f"confidence {band:13} {len(bands[band]):3} lines  CER {rate:.3f}")
text
HTR against alice: CER 0.087 (150 edits, 88 lines)
HTR against bob: CER 0.106 (184 edits, 88 lines)
bob against alice: CER 0.021 (37 edits, 29 of 88 lines differ)
confidence below 0.90     15 lines  CER 0.255
confidence 0.90 to 0.95   21 lines  CER 0.140
confidence 0.95 and up    52 lines  CER 0.042

Kraken のテキストの CER は、Alice の修正に対して 8.7%、Bob の修正に対して 10.6% でした。スクリプトの 150 件の編集と ketos test の 151 件が一つ異なるのは、Kraken の行の一つが空白で終わっており、二つの集計が空白を異なる扱いにするためです。二人の転写者の間の 37 件の編集は、Bob が省いた 37 個の記号であり、規約の違いだけで、モデルの測定された CER が 1.9 ポイント動きました。最初の行を修正する前に規約を揃え、CER は一人の転写者のテキストに対して、または二人の転写者が一緒に確定したテキストに対して報告してください。

信頼度 0.90 未満の行の CER は 25.5% で、0.95 以上の行の 4.2% と比べて高くなっています。一行には信頼度がありません。Kraken が一文字だけの行で何も読み取らなかったためで、スクリプトはその行を 0.90 未満に数えます。それでも、0.95 以上の 52 行のうち 24 行には、少なくとも一つの誤りがありました。信頼度の高い行を飛ばすプロジェクトは、それらの誤りを学習データに残すことになります。

修正済みテキストからの TEI

Potato 2.13.1 には、text_edit プロジェクトから TEI への経路がありません。tei エクスポートは、スパンのアノテーションを TEI の名前要素として書き出し、このプロジェクトではエクスポートはエラーで停止します。

text
Export failed!
  ERROR: Cannot export: No span annotations to export

tei エクスポーターは、各項目の修正済みテキストではなく元のテキストも書き出すため、設定にスパンスキームを追加しても、出力は Kraken の読みのままです。したがって、修正済みの行から TEI を作るには、Potato の外での変換が必要です。Transkribus のヘルプセンターは、Scholar、Team、Organisation の各プランに TEI エクスポートがあると記載しており、二つのスタイルシートのうち一つが page2tei です。page2tei のスタイルシートは MIT ライセンスで、Transkribus の外で動作します。このスタイルシートは Transkribus のエクスポートの METS ファイルから始まりますが、Potato のエクスポートは PAGE ファイルを書き出し、METS ファイルは書き出しません。通常の読み用テキストなら、短いスクリプトで、PAGE ファイルから修正済みの各 TextLine を読み順に読み込み、各行の前に TEI の <lb/> を書き出せます。

HTR 修正のための他のツール

Transkribus と eScriptorium にはそれぞれ修正エディターがあり、この二つのプラットフォームは、テキストの認識とモデルの学習も行います。Transkribus は READ-COOP が提供するホスト型サービスです。Transkribus の料金プランのページには、認識は 1 ページにつき 1 クレジット、無料プランは月 50 クレジットで PAGE XML を含む基本的なエクスポートが使えること、Scholar プランは年額 99 ユーロで 900 クレジットとすべてのエクスポート形式が使えることが記載されています。eScriptorium は MIT ライセンスで、プロジェクトがホストするサーバーまたは機関のインスタンスで Kraken を実行し、PAGE XML、ALTO、プレーンテキストをエクスポートします。再学習の前に一人でモデルの出力を修正する場合は、モデルを実行したツールのエディターのほうが近道です。

Potato は、修正を複数の転写者で分担する場合、各行が複数の転写者に渡る場合、ページ画像を自分のサーバーに置いたままにする場合に適しています。デジタル人文学の転写ツールの比較は、Transkribus、eScriptorium、FromThePage、Zooniverse、Potato を並べて比較しています。

HTR 修正における Potato の限界

  • 認識も学習もありません。 Potato は Transkribus、eScriptorium、OCR-D、Kraken の PAGE XML を読み込み、HTR モデルは実行しません。
  • 転写の一致度の値がありません。 Potato 2.13.1 は text_edit の回答について一致度を計算せず、上の CER スクリプトがその代わりになります。
  • ALTO には対応していません。 Potato 2.13.1 は PAGE XML を読み書きし、ALTO XML は扱いません。インポートには、Transkribus または eScriptorium から PAGE XML をエクスポートしてください。
  • 修正済みの行からの TEI はありません。 tei エクスポートは、上の TEI のセクションで説明したとおり、元のテキストの上のスパンを書き出します。
  • 単語単位の差分。 「Changes」ビューは単語全体に印を付け、「Chars changed」は文字数を示します。

次に読むもの

参考文献

  1. Sánchez, J. A., Romero, V., Toselli, A. H., & Vidal, E. (2016). ICFHR2016 Competition on Handwritten Text Recognition on the READ Dataset. 15th International Conference on Frontiers in Handwriting Recognition (ICFHR), 630-635. https://doi.org/10.1109/ICFHR.2016.0120
  2. Toselli, A. H., Romero, V., Villegas, M., Vidal, E., & Sánchez, J. A. (2018). HTR Dataset ICFHR 2016 (Version 1.2.0). Zenodo. https://doi.org/10.5281/zenodo.1297399
  3. Weil, S. (2023). HTR model for German manuscripts trained from several datasets. Zenodo. https://doi.org/10.5281/zenodo.7933463