فاز 4 · درس 19

آموزش جامع OCR و درک ساختار اسناد با پایتون

نوع
مفهومی و کاربردی
زمان
حدود 45 دقیقه
زبان‌ها
Python
اسکنر نوری که نور خطی باریکی را روی یک سند کاغذی پر از جدول و متن می‌تاباند و بخش‌هایی از متن را برجسته می‌کند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. پایپ‌لاین کلاسیک
  5. CTC در یک پاراگراف
  6. مدل‌های سرتاسری مدرن
  7. تحلیل چیدمان
  8. معیارهای ارزیابی
  9. پیاده‌سازی عملی
  10. گام 1: تابع زیان CTC به‌همراه دکودر حریصانه
  11. گام 2: بازشناس کوچک CRNN
  12. گام 3: ساخت داده‌های مصنوعی برای OCR
  13. گام 4: طرح کلی آموزش
  14. استفاده از ابزارهای آماده
  15. خروجی این درس
  16. تمرین‌ها
  17. اصطلاحات کلیدی
  18. برای مطالعه‌ی بیشتر

استخراج اطلاعات از تصاویر اسناد، فاکتورها، رسیدها و مدارک شناسایی یکی از پرتقاضاترین وظایف مهندسی هوش مصنوعی است. تفاوت بزرگی میان استخراج ساده‌ی نویسه‌ها از روی تصویر و درک ساختار مفهومی آن — مانند خواندن مبلغ نهایی یک فاکتور — وجود دارد که اغلب تیم‌های فنی در شروع کار آن را نادیده می‌گیرند.

در این درس پایپ‌لاین سنتی OCR (شامل تشخیص، بازشناسی و بازسازی چیدمان) را در کنار مدل‌های نوین سرتاسری بررسی می‌کنیم. همچنین با پیاده‌سازی عملی معماری پایدار CRNN و تابع زیان CTC، مکانیسم تبدیل ویژگی‌های تصویری به توالی کاراکترها بدون نیاز به برچسب‌گذاری جزئی را یاد می‌گیرید.

اگر برای دانلود مدل‌های حجیم بینایی-زبانی یا نصب پکیج‌هایی مانند PaddleOCR با اختلال در مخازن روبه‌رو شدید، بهره‌گیری از میرورهای محلی PyPI و اجرای اولیه‌ی مدل روی CPU یا پردازنده‌های سازگار می‌تواند فرآیند تست را بسیار سریع‌تر کند.

نویسه‌خوان نوری یا OCR یک پایپ‌لاین سه‌مرحله‌ای است: تشخیص کادرهای متنی، بازشناسی نویسه‌ها و سپس چیدمان آن‌ها. تمام سیستم‌های مدرن OCR این مراحل را بازآرایی می‌کنند یا در یکدیگر ادغام (merge) می‌نمایند.

اهداف یادگیری

  • بررسی پایپ‌لاین کلاسیک OCR (تشخیص -> بازشناسی -> چیدمان) و جایگزین‌های سرتاسری و مدرن (Donut و Qwen-VL-OCR)
  • پیاده‌سازی تابع زیان (loss) مدل CTC (دسته‌بندی زمانی پیوندگرا یا Connectionist Temporal Classification) برای آموزش توالی‌به‌توالی OCR
  • استفاده از PaddleOCR یا EasyOCR برای پارس اسناد در محیط پروداکشن بدون نیاز به آموزش
  • تفکیک OCR، تحلیل چیدمان و درک سند؛ و انتخاب ابزار مناسب برای هر کار

صورت مسئله

تصاویر پر از متن همه‌جا هستند: رسیدها، فاکتورها، کارت‌های شناسایی، کتاب‌های اسکن‌شده، فرم‌ها، وایت‌بردها، تابلوها و اسکرین‌شات‌ها. استخراج داده‌های ساخت‌یافته از آن‌ها — نه فقط خواندن نویسه‌ها، بلکه فهمیدن این‌که «این مقدار، مبلغ کل است» — یکی از باارزش‌ترین مسائل کاربردی در بینایی ماشین به شمار می‌رود.

این حوزه به سه لایه‌ی مهارتی تقسیم می‌شود:

  1. خودِ OCR: تبدیل پیکسل‌ها به متن.
  2. تحلیل چیدمان (Layout parsing): گروه‌بندی خروجی OCR در قالب بخش‌های مختلف (عنوان، بدنه، جدول، سربرگ).
  3. درک سند (Document understanding): استخراج فیلدهای ساخت‌یافته (مثل «invoice_total = $42.50») از دل چیدمان متن.

هر لایه رویکردهای کلاسیک و مدرن خودش را دارد؛ فاصله‌ی میان «می‌خواهم متن را از تصویر استخراج کنم» تا «به مبلغ کل این رسید نیاز دارم» بسیار بیشتر از آن چیزی است که بیشتر تیم‌ها تصور می‌کنند.

مفاهیم پایه

پایپ‌لاین کلاسیک

تصویرتشخیص متن(DB, EAST, CRAFT)کادرهای احاطه‌کنندهکلمه/خطبرش هر ناحیهبازشناسی(CRNN + CTC)رشته‌های متنیمرتب‌سازیچینشمتن با ترتیب خواندن
  • تشخیص متن (Text detection) چهارضلعی‌های مربوط به هر خط یا کلمه را استخراج می‌کند.
  • بازشناسی (Recognition) هر بخش را به ارتفاعی ثابت برش می‌زند و با اجرای CNN + BiLSTM + CTC، توالی نویسه‌ها را می‌سازد.
  • چیدمان (Layout) ترتیب خوانش متن را بازسازی می‌کند (از بالا به پایین و چپ به راست برای خط لاتین؛ و متفاوت برای خطوطی مثل عربی یا ژاپنی).

CTC در یک پاراگراف

بخش بازشناسی OCR، از یک نقشه‌ی ویژگی با طول ثابت، توالی متغیری از نویسه‌ها تولید می‌کند. روش CTC (گریوز و همکاران، 2006) به شما امکان می‌دهد این مدل را بدون نیاز به ترازسازی (alignment) در سطح نویسه آموزش دهید. مدل در هر گام زمانی، توزیع احتمالی روی (واژگان + نویسه‌ی خالی یا blank) ارائه می‌دهد؛ سپس تابع زیان (loss) مدل CTC تمام ترازهایی را که پس از ادغام نویسه‌های تکراری و حذف جاهای خالی به متن هدف تبدیل می‌شوند، تجمیع آماری (marginalise) می‌کند.

raw output: "h h h _ _ e e l l _ l l o _ _"
after merge repeats and remove blanks: "hello"

همین CTC دلیل موفقیت معماری CRNN در سال 2015 بود و هنوز هم در سال 2026 پایه‌ی آموزش بیشتر مدل‌های پروداکشن OCR به شمار می‌رود.

مدل‌های سرتاسری مدرن

  • Donut (کیم و همکاران، 2022) — ترکیبی از یک انکودر ViT و یک دکودر متنی؛ تصویر را می‌خواند و مستقیماً JSON برمی‌گرداند. نیازی به ماژول جداگانه‌ی تشخیص متن یا چیدمان ندارد.
  • TrOCR — ترکیب ViT با دکودر ترنسفورمر برای OCR در سطح خطوط متن.
  • Qwen-VL-OCR / InternVL — مدل‌های زبانی‌دیداری کامل (VLM) که برای کارهای OCR فاین‌تیون شده‌اند؛ بالاترین دقت سال 2026 را روی اسناد پیچیده دارند.
  • PaddleOCR — پایپ‌لاین کلاسیک DB + CRNN در قالب بسته‌ای پایدار و آماده‌ی پروداکشن؛ همچنان ابزار اصلی و پرکاربرد دنیای متن‌باز.

مدل‌های سرتاسری به داده و توان پردازشی بیشتری نیاز دارند، اما در عوض گرفتار تجمع خطاهای پایپ‌لاین‌های چندمرحله‌ای نمی‌شوند.

تحلیل چیدمان

برای اسناد ساخت‌یافته، یک مدل چیدمان (مثل LayoutLMv3 یا آشکارسازی که روی مجموعه‌داده‌ی DocLayNet آموزش دیده) اجرا می‌کنید تا به هر بخش برچسب بزند: عنوان، پاراگراف، تصویر، جدول یا پانویس. ترتیب خوانش سند هم در ادامه به سادگی تبدیل می‌شود به «پیمایش بخش‌ها به ترتیب چیدمان و الحاق متن آن‌ها».

برای فرم‌ها، سراغ مدل‌های استخراج کلید-مقدار (Key-Value extraction) بروید (Donut برای اسناد غنی از تصویر، و LayoutLMv3 برای اسکن‌های متنی ساده). این مدل‌ها تصویر، متن شناسایی‌شده و موقعیت کلمات را می‌گیرند و جفت‌های کلید-مقدار ساخت‌یافته را تحویل می‌دهند.

معیارهای ارزیابی

  • نرخ خطای نویسه (CER) — فاصله‌ی لون‌اشتاین تقسیم بر طول متن مرجع. هرچه کمتر باشد بهتر است. هدف پروداکشن: کمتر از 2% روی اسکن‌های تمیز.
  • نرخ خطای کلمه (WER) — همان مفهوم در سطح کلمات.
  • امتیاز F1 روی فیلدهای ساخت‌یافته — برای وظایف استخراج کلید-مقدار؛ بررسی می‌کند که آیا مثلاً {invoice_total: 42.50} درست استخراج شده است یا خیر.
  • فاصله‌ی ویرایشی روی JSON — برای پارس سرتاسری اسناد؛ مقاله‌ی Donut مفهوم فاصله‌ی ویرایشی درختیِ نرمال‌شده را برای این کار معرفی کرد.
OCR · فشرده‌سازی CTCتبدیل برچسب‌های فریم به یک رشته واحد
OCR · CTC COLLAPSEA CRNN emits one character (or a blank ε) per time-step, with no fixed alignment to the input. CTC defines the collapse: merge runs of the same label, then delete the blanks. The blank between the two l-runs is what keeps "hello" from collapsing to "helo". Training sums over every alignment that collapses to the target.پیش‌بینی‌های CRNN به ازای هر فریم (هر ستون یک گام زمانی)hhεellεlooقاعده 1 · merge کردن همسایه‌های تکراریقاعده 2 · حذف نماد تهی εhelloh h ε e l l ε l o o -> "hello"
یک شبکه CRNN در هر گام زمانی یک کاراکتر (یا نماد تهی ε) صادر می‌کند، بدون اینکه تراز ثابتی با ورودی داشته باشد. روش CTC عمل فشرده‌سازی را تعریف می‌کند: توالی‌های متوالی از برچسب یکسان را merge کرده و سپس نمادهای تهی را حذف می‌کند. وجود نماد تهی بین دو دنباله l مانع از فشرده شدن «hello» به «helo» می‌شود. آموزش بر روی تمام ترازهایی که به رشته هدف فشرده می‌شوند، مجموع می‌گیرد.

پیاده‌سازی عملی

گام 1: تابع زیان CTC به‌همراه دکودر حریصانه

import torch
import torch.nn as nn
import torch.nn.functional as F


def ctc_loss(log_probs, targets, input_lengths, target_lengths, blank=0):
    """
    log_probs:      (T, N, C) log-softmax over vocab including blank at index 0
    targets:        (N, S) int targets (no blanks)
    input_lengths:  (N,) per-sample time steps used
    target_lengths: (N,) per-sample target length
    """
    return F.ctc_loss(log_probs, targets, input_lengths, target_lengths,
                      blank=blank, reduction="mean", zero_infinity=True)


def greedy_ctc_decode(log_probs, blank=0):
    """
    log_probs: (T, N, C) log-softmax
    returns: list of index sequences (blanks removed, repeats merged)
    """
    preds = log_probs.argmax(dim=-1).transpose(0, 1).cpu().tolist()
    out = []
    for seq in preds:
        decoded = []
        prev = None
        for idx in seq:
            if idx != prev and idx != blank:
                decoded.append(idx)
            prev = idx
        out.append(decoded)
    return out

تابع F.ctc_loss در صورت در دسترس بودن، از پیاده‌سازی بهینه‌ی CuDNN استفاده می‌کند. دکودر حریصانه بسیار ساده‌تر از جستجوی پرتو (beam search) است و معمولاً اختلاف CER آن با جستجوی پرتو کمتر از 1% است.

گام 2: بازشناس کوچک CRNN

یک ترکیب مینیمال از CNN + BiLSTM برای OCR در سطح خطوط متن.

class TinyCRNN(nn.Module):
    def __init__(self, vocab_size=40, hidden=128, feat=32):
        super().__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1, feat, 3, 1, 1), nn.BatchNorm2d(feat), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(feat, feat * 2, 3, 1, 1), nn.BatchNorm2d(feat * 2), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(feat * 2, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),
            nn.Conv2d(feat * 4, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),
        )
        self.rnn = nn.LSTM(feat * 4, hidden, bidirectional=True, batch_first=True)
        self.head = nn.Linear(hidden * 2, vocab_size)

    def forward(self, x):
        # x: (N, 1, H, W)
        f = self.cnn(x)                # (N, C, H', W')
        f = f.mean(dim=2).transpose(1, 2)  # (N, W', C)
        h, _ = self.rnn(f)
        return F.log_softmax(self.head(h).transpose(0, 1), dim=-1)  # (W', N, vocab)

ورودی ارتفاع ثابتی دارد (لایه CNN با max-pooling ارتفاع را به 1 می‌رساند). عرض تصویر همان بعد زمان برای الگوریتم CTC است.

گام 3: ساخت داده‌های مصنوعی برای OCR

رشته‌هایی با متن مشکی روی پس‌زمینه‌ی سفید می‌سازیم تا یک تست صحت‌سنجی (smoke test) سرتاسری اجرا کنیم. هر نویسه به ستونی از شش نوار تبدیل می‌شود که ایندکس واژگانی آن را به صورت باینری نمایش می‌دهد. تصویر باید هویت نویسه را به دوش بکشد؛ اگر همه‌ی نویسه‌ها شبیه یک کادر خالی بودند، مدل نمی‌توانست a را از x تشخیص دهد و کاهش تابع زیان (loss) متوقف می‌شد.

import numpy as np

GLYPHS = "0123456789abcdefghijklmnopqrstuvwxyz"

def synthetic_line(text, height=32, char_width=16):
    W = char_width * len(text)
    img = np.ones((height, W), dtype=np.float32)
    for i, c in enumerate(text):
        x = i * char_width
        code = GLYPHS.find(c) + 1
        for bit in range(6):
            if code >> bit & 1:
                y = 4 + 4 * bit
                img[y:y + 3, x + 2:x + char_width - 2] = 0.0
    return img


def build_batch(strings, vocab):
    H = 32
    W = 16 * max(len(s) for s in strings)
    imgs = np.ones((len(strings), 1, H, W), dtype=np.float32)
    target_lengths = []
    targets = []
    for i, s in enumerate(strings):
        imgs[i, 0, :, :16 * len(s)] = synthetic_line(s)
        ids = [vocab.index(c) for c in s]
        targets.extend(ids)
        target_lengths.append(len(ids))
    return torch.from_numpy(imgs), torch.tensor(targets), torch.tensor(target_lengths)


vocab = ["_"] + list("0123456789abcdefghijklmnopqrstuvwxyz")
imgs, targets, lengths = build_batch(["hello", "world"], vocab)
print(f"images: {imgs.shape}   targets: {targets.shape}   lengths: {lengths.tolist()}")

یک مجموعه‌داده‌ی واقعی OCR تنوع فونت، نویز، چرخش، تاری و رنگ را اضافه می‌کند، ولی ساختار پایپ‌لاین دقیقاً به همین شکل باقی می‌ماند.

گام 4: طرح کلی آموزش

model = TinyCRNN(vocab_size=len(vocab))
opt = torch.optim.Adam(model.parameters(), lr=1e-3)

for step in range(400):
    strings = ["abc" + str(step % 10)] * 4 + ["xyz" + str((step + 1) % 10)] * 4
    imgs, targets, target_lens = build_batch(strings, vocab)
    log_probs = model(imgs)  # (W', 8, vocab)
    input_lens = torch.full((8,), log_probs.size(0), dtype=torch.long)
    loss = ctc_loss(log_probs, targets, input_lens, target_lens, blank=0)
    opt.zero_grad(); loss.backward(); opt.step()

روی این داده‌های ساده‌ی ساختگی، مقدار تابع زیان (loss) باید طی 400 گام از اعداد دو رقمی به حدود 0.3 برسد. فایل code/main.py همین حلقه را روی مجموعه‌ی رشته‌های خود اجرا می‌کند و در پایان سه رشته‌ی آزمایشی را دکود می‌نماید.

استفاده از ابزارهای آماده

سه راهکار پروداکشن پیش رو دارید:

  • PaddleOCR — بالغ، سریع و چندزبانه. استفاده در یک خط کد: paddleocr.PaddleOCR(lang="en").ocr(image_path).
  • EasyOCR — ابزاری مبتنی بر پایتون و PyTorch، با پشتیبانی از زبان‌های گوناگون.
  • Tesseract — ابزاری کلاسیک؛ همچنان برای اسناد اسکن‌شده‌ی قدیمی که مدل‌های امروزی رویشان به مشکل می‌خورند، نجات‌بخش است.

برای پارس سرتاسری اسناد، از Donut یا یک VLM استفاده کنید:

from transformers import DonutProcessor, VisionEncoderDecoderModel

processor = DonutProcessor.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2")
model = VisionEncoderDecoderModel.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2")

برای رسیدها، فاکتورها و فرم‌هایی با ساختار تکرارشونده، Donut را فاین‌تیون کنید. برای اسناد متفرقه یا مواردی از OCR که نیاز به تحلیل و استدلال دارند، یک VLM مانند Qwen-VL-OCR گزینه‌ی پیش‌فرض امروزی است.

خروجی این درس

از این درس خروجی‌های زیر به دست می‌آید:

  • outputs/prompt-ocr-stack-picker.md — پرامپتی که بر اساس نوع سند، زبان و ساختار آن، ابزار مناسب را از بین Tesseract ،PaddleOCR ،Donut یا VLM-OCR انتخاب می‌کند.
  • outputs/skill-ctc-decoder.md — مهارتی برای پیاده‌سازی دکودرهای CTC حریصانه و beam search از صفر، همراه با نرمال‌سازی طول.

تمرین‌ها

  1. (ساده) مدل TinyCRNN را روی رشته‌های عددی تصادفی 5 رقمی به مدت 500 گام آموزش دهید. نرخ خطای نویسه (CER) را روی داده‌های آزمون مجزا گزارش کنید.
  2. (متوسط) دکودر حریصانه را با جستجوی پرتو (با beam_width=5) جایگزین کنید. میزان تغییر CER را گزارش دهید. جستجوی پرتو روی چه نوع ورودی‌هایی نتیجه‌ی بهتری می‌دهد؟
  3. (سخت) با استفاده از PaddleOCR روی مجموعه‌ای از 20 رسید، اقلام فاکتور را استخراج کنید و امتیاز F1 را برای جفت‌های {item_name, price} در مقایسه با داده‌های مرجع برچسب‌گذاری‌شده‌ی دستی به دست آورید.

اصطلاحات کلیدی

اصطلاح در گفت‌وگوهای عامیانه معنای دقیق فنی
OCR «استخراج متن از پیکسل‌ها» تبدیل بخش‌هایی از تصویر به توالی نویسه‌ها
CTC «تابع زیانِ بدون نیاز به ترازسازی» تابع زیانی (loss) که مدل توالی را بدون برچسب‌گذاری در هر گام زمانی آموزش می‌دهد؛ محاسبات را روی تمام ترازهای ممکن جمع‌بندی حاشیه‌ای می‌کند
CRNN «مدل کلاسیک OCR» استخراج‌کننده‌ی ویژگی کانولوشنی + BiLSTM + لایه‌ی CTC؛ معماری پایه‌ی سال 2015 که هنوز در پروداکشن کاربرد دارد
Donut «OCR سرتاسری» انکودر ViT + دکودر متنی؛ ساختار JSON را مستقیماً از روی تصویر استخراج می‌کند
تحلیل چیدمان «یافتن بخش‌ها» تشخیص و برچسب‌گذاری بخش‌های عنوان، جدول، تصویر و پاراگراف در سند
ترتیب خوانش «توالی متن» مرتب‌سازی بخش‌های شناسایی‌شده در قالب جملات پیوسته؛ در خط لاتین ساده است اما در چیدمان‌های ترکیبی چالش دارد
CER / WER «نرخ‌های خطا» فاصله‌ی لون‌اشتاین تقسیم بر طول مرجع، در سطح نویسه یا کلمه
VLM-OCR «مدل زبانی تصویرخوان» مدل دیداری‌زبانی که برای وظایف OCR آموزش دیده یا پرامپت شده است؛ بهترین راهکار فعلی روی اسناد پیچیده

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا OCR به جای آنتروپی متقاطع ساده از تابع زیان (loss) بنام CTC استفاده می‌کند؟
  2. پیش از درسدر رمزگشایی CTC، نقش توکن تهی (blank) چیست؟
  3. پس از درسچرا رمزگشایی حریصانه (greedy) در CTC گاهی روی مجموعه‌داده‌های ساده عملکرد بهتری نسبت به beam search دارد؟
  4. پس از درسمدل Donut چگونه تشخیص متن و بازشناسی کاراکتر صریح را نادیده می‌گیرد و حذف می‌کند؟
  5. پس از درسبرای استخراج `invoice_total` از رسیدها، کدام روش معمولاً در سال 2026 بهترین است؟

سؤالات متداول

تفاوت اصلی بین OCR سنتی و درک سند (Document Understanding) چیست؟

ابزار OCR صرفاً پیکسل‌های تصویر را به متن یا توالی نویسه‌ها تبدیل می‌کند؛ در حالی که سیستم درک سند، ساختار و معنای هر بخش را متوجه می‌شود و فیلدهای کلید-مقدار مثل مبلغ یا نام خریدار را به‌طور تفکیک‌شده استخراج می‌کند.

تابع زیان CTC در مدل‌های OCR چه نقشی دارد؟

روش CTC امکان آموزش مدل‌های تبدیل تصویر به متن را بدون نیاز به ترازسازی دقیق هر حرف با فریم تصویر فراهم می‌کند و با جمع‌بندی تمام حالت‌های ممکن، خروجی را با متن هدف منطبق می‌سازد.

چه زمانی باید از PaddleOCR و چه زمانی از Donut استفاده کرد؟

برای تشخیص خطوط متن عمومی و اجرای سریع، کم‌هزینه و پروداکشن، ابزار PaddleOCR گزینه‌ی استاندارد است؛ اما برای اسناد ساخت‌یافته مانند فرم‌ها و فاکتورها که نیاز به تولید مستقیم خروجی JSON دارند، مدل Donut کاربرد بهتری دارد.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.