Complete Guide to Finetuning EasyOCR for Custom Datasets

# Tutorial Lengkap Finetuning EasyOCR untuk Dataset Custom EasyOCR adalah library OCR (Optical Character Recognition) open-source yang powerful dan mendukung lebih dari 80 bahasa. Namun, untuk kasus...

By Ruby Abdullah · · tutorial
EasyOCROCRComputer VisionDeep LearningFinetuning

Complete Guide to Finetuning EasyOCR for Custom Datasets

EasyOCR is a powerful open-source OCR (Optical Character Recognition) library that supports over 80 languages. However, for specific use cases such as custom fonts, historical documents, handwriting, or unique document formats, finetuning the EasyOCR model can significantly improve accuracy.

In this tutorial, we will learn how to perform EasyOCR finetuning from scratch to model evaluation.

Prerequisites

Before starting, ensure you have:

  • Python 3.7+ (Python 3.8 or 3.9 recommended)
  • GPU with CUDA support (highly recommended, minimum 8GB VRAM)
  • At least 16GB system RAM
  • Image dataset with ground truth labels (minimum 1000 samples for good results)
  • Minimum 10GB disk space for models and dataset

Installation and Environment Setup

1. Clone EasyOCR Repository

# Clone repository

git clone https://github.com/JaidedAI/EasyOCR.git

cd EasyOCR

Checkout to stable branch (optional)

git checkout v1.7.0

2. Setup Virtual Environment

# Create virtual environment

python -m venv easyocrenv

Activate

For Linux/Mac:

source easyocrenv/bin/activate

For Windows:

easyocrenv\Scripts\activate

3. Install Dependencies

# Install PyTorch with CUDA support

Adjust CUDA version to match your system

pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118

Install EasyOCR requirements

pip install -r requirements.txt

Install additional training dependencies

pip install tensorboard

pip install lmdb

pip install pillow

pip install opencv-python

pip install albumentations

pip install python-Levenshtein

4. Verify Installation

import torch

print(f"PyTorch version: {torch.version}")

print(f"CUDA available: {torch.cuda.isavailable()}")

print(f"CUDA version: {torch.version.cuda}")

print(f"GPU count: {torch.cuda.devicecount()}")

if torch.cuda.isavailable():

print(f"GPU name: {torch.cuda.getdevicename(0)}")

Dataset Preparation

The dataset is the most important component in finetuning. Dataset quality and quantity significantly affect the final results.

1. Dataset Folder Structure

Create folder structure like this:

dataset/

├── raw/

│ ├── train/

│ │ ├── img001.jpg

│ │ ├── img002.jpg

│ │ └── ...

│ ├── validation/

│ │ ├── img001.jpg

│ │ └── ...

│ └── test/

│ ├── img001.jpg

│ └── ...

├── labels/

│ ├── trainlabels.txt

│ ├── vallabels.txt

│ └── testlabels.txt

└── lmdb/

├── train/

└── validation/

2. Label File Format

Label files use TSV (Tab-Separated Values) format:

train
labels.txt:
img001.jpg	Hello World

img002.jpg Invoice #12345

img003.jpg Total: $1,250.00

img004.jpg PT Rubythalib Data Konsulta

Each line contains:

  • Image filename
  • Tab character (\t)
  • Ground truth text

Tips for Quality Dataset:
  • Images must be clear and readable
  • Minimum resolution 64x256 pixels
  • Varied fonts, sizes, and styles
  • Various lighting conditions
  • Include realistic noise and distortion
  • Balanced character distribution

3. Dataset Preprocessing Script

Create file preparedataset.py:

import os

import cv2

import numpy as np

from PIL import Image

from pathlib import Path

def preprocessimage(imagepath, outputpath):

"""

Image preprocessing for OCR training

"""

# Read image

img = cv2.imread(str(imagepath))

if img is None:

print(f"Error reading {imagepath}")

return False

# Convert to grayscale

gray = cv2.cvtColor(img, cv2.COLORBGR2GRAY)

# Noise reduction

denoised = cv2.fastNlMeansDenoising(gray, None, 10, 7, 21)

# Adaptive thresholding (optional, depends on dataset)

# thresh = cv2.adaptiveThreshold(denoised, 255,

# cv2.ADAPTIVETHRESHGAUSSIANC,

# cv2.THRESHBINARY, 11, 2)

# Resize if too large (max height 128)

height, width = denoised.shape

if height > 128:

scale = 128 / height

newwidth = int(width scale)

denoised = cv2.resize(denoised, (newwidth, 128))

# Save

cv2.imwrite(str(outputpath), denoised)

return True

def validatedataset(labelfile, imagedir):

"""

Validate dataset - check all images exist and are readable

"""

issues = []

with open(labelfile, 'r', encoding='utf-8') as f:

lines = f.readlines()

print(f"Validating {len(lines)} samples...")

for idx, line in enumerate(lines):

parts = line.strip().split('\t')

if len(parts) != 2:

issues.append(f"Line {idx+1}: Invalid format (expected 2 columns)")

continue

imagename, text = parts

imagepath = os.path.join(imagedir, imagename)

# Check file exists

if not os.path.exists(imagepath):

issues.append(f"Line {idx+1}: Image not found - {imagename}")

continue

# Check image readable

img = cv2.imread(imagepath)

if img is None:

issues.append(f"Line {idx+1}: Cannot read image - {imagename}")

continue

# Check text not empty

if len(text.strip()) == 0:

issues.append(f"Line {idx+1}: Empty text label - {imagename}")

if issues:

print(f"\n❌ Found {len(issues)} issues:")

for issue in issues[:10]: # Show first 10

print(f" - {issue}")

if len(issues) > 10:

print(f" ... and {len(issues)-10} more")

return False

else:

print(f"✅ All {len(lines)} samples validated successfully!")

return True

Run validation

if name == "main":

# Validate training set

validatedataset(

"dataset/labels/trainlabels.txt",

"dataset/raw/train"

)

# Validate validation set

validatedataset(

"dataset/labels/vallabels.txt",

"dataset/raw/validation"

)

Run validation:

python preparedataset.py

4. Convert Dataset to LMDB Format

EasyOCR uses LMDB (Lightning Memory-Mapped Database) for more efficient training. Create file createlmdbdataset.py:

import os

import lmdb

import cv2

import numpy as np

from tqdm import tqdm

def createlmdbdataset(imagedir, labelfile, outputpath):

"""

Create LMDB dataset for EasyOCR training

Args:

imagedir: Directory containing images

labelfile: Path to label file (TSV format)

outputpath: Output LMDB database path

"""

# Read label file

with open(labelfile, 'r', encoding='utf-8') as f:

lines = [line.strip() for line in f.readlines()]

# Calculate map size (1TB)

mapsize = 1099511627776

# Create LMDB environment

env = lmdb.open(outputpath, mapsize=mapsize)

cache = {}

cnt = 1

print(f"Processing {len(lines)} samples...")

for idx, line in enumerate(tqdm(lines)):

# Parse line

parts = line.split('\t')

if len(parts) != 2:

print(f"Skipping line {idx+1}: Invalid format")

continue

imagename, label = parts

imagepath = os.path.join(imagedir, imagename)

# Check file exists

if not os.path.exists(imagepath):

print(f"Skipping line {idx+1}: Image not found - {imagename}")

continue

# Read image as binary

with open(imagepath, 'rb') as f:

imagebin = f.read()

if not imagebin:

print(f"Skipping line {idx+1}: Empty image - {imagename}")

continue

# Create keys (format required by EasyOCR)

imagekey = f'image-{cnt:09d}'.encode()

labelkey = f'label-{cnt:09d}'.encode()

# Store in cache

cache[imagekey] = imagebin

cache[labelkey] = label.encode('utf-8')

cnt += 1

# Write to LMDB in batches (every 1000 samples)

if cnt % 1000 == 0:

with env.begin(write=True) as txn:

for key, value in cache.items():

txn.put(key, value)

cache = {}

print(f"Written {cnt-1} samples to LMDB")

# Write remaining cache

if cache:

with env.begin(write=True) as txn:

for key, value in cache.items():

txn.put(key, value)

# Write total number of samples

with env.begin(write=True) as txn:

txn.put('num-samples'.encode(), str(cnt-1).encode())

env.close()

print(f"\n✅ Created LMDB dataset with {cnt-1} samples at {outputpath}")

return cnt-1

def verifylmdb(lmdbpath):

"""

Verify LMDB dataset

"""

env = lmdb.open(lmdbpath, readonly=True, lock=False)

with env.begin() as txn:

numsamples = txn.get('num-samples'.encode())

if numsamples:

numsamples = int(numsamples.decode())

print(f"Total samples in LMDB: {numsamples}")

# Check some samples

print("\nChecking first 3 samples:")

for i in range(1, min(4, numsamples+1)):

imagekey = f'image-{i:09d}'.encode()

labelkey = f'label-{i:09d}'.encode()

imagebin = txn.get(imagekey)

label = txn.get(labelkey)

if imagebin and label:

print(f" Sample {i}: Label = '{label.decode('utf-8')}', Image size = {len(imagebin)} bytes")

else:

print(f" Sample {i}: Missing data!")

env.close()

if name == "main":

# Create training LMDB

print("Creating training LMDB...")

traincount = createlmdbdataset(

imagedir="dataset/raw/train",

labelfile="dataset/labels/trainlabels.txt",

outputpath="dataset/lmdb/train"

)

print("\nVerifying training LMDB...")

verifylmdb("dataset/lmdb/train")

print("\n" + "="50 + "\n")

# Create validation LMDB

print("Creating validation LMDB...")

valcount = createlmdbdataset(

imagedir="dataset/raw/validation",

labelfile="dataset/labels/vallabels.txt",

outputpath="dataset/lmdb/validation"

)

print("\nVerifying validation LMDB...")

verifylmdb("dataset/lmdb/validation")

print("\n" + "="50)

print(f"✅ Dataset ready for training!")

print(f" Training samples: {traincount}")

print(f" Validation samples: {valcount}")

print(f" Ratio: {valcount/traincount100:.1f}% validation")

Run conversion:

python createlmdbdataset.py

Expected output:

Processing 5000 samples...

100%|████████████████████| 5000/5000 [00:15<00:00, 320.45it/s]

✅ Created LMDB dataset with 5000 samples at dataset/lmdb/train

Verifying training LMDB...

Total samples in LMDB: 5000

Checking first 3 samples:

Sample 1: Label = 'Hello World', Image size = 45231 bytes

Sample 2: Label = 'Invoice #12345', Image size = 38942 bytes

Sample 3: Label = 'Total: $1,250.00', Image size = 41523 bytes

Training Configuration

1. Understanding EasyOCR Model Architecture

EasyOCR uses a 4-stage architecture:

  • Transformation: TPS (Thin Plate Spline) - handles text distortion
  • Feature Extraction: ResNet, VGG, or RCNN - visual feature extraction
  • Sequence Modeling: BiLSTM or LSTM - sequence modeling
  • Prediction: CTC or Attention - prediction decoding
  • 2. Training Configuration File

    Create file trainconfig.yaml:

    # Data configuration
    

    data:

    traindata: "./dataset/lmdb/train"

    validdata: "./dataset/lmdb/validation"

    selectdata: ["train"]

    batchratio: [1.0]

    totaldatausageratio: 1.0 # 1.0 = use all data

    batchmaxlength: 25 # max text length

    imgH: 64 # image height

    imgW: 256 # image width

    rgb: False # False = grayscale, True = RGB

    character: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()+,-./:;<=>?@[\\]^{|}~ " # character set

    batchsize: 192

    numworkers: 4

    Model architecture

    model:

    inputchannel: 1 # 1 for grayscale, 3 for RGB

    outputchannel: 512

    hiddensize: 256

    Transformation: "TPS" # None | TPS

    FeatureExtraction: "ResNet" # VGG | RCNN | ResNet

    SequenceModeling: "BiLSTM" # None | BiLSTM

    Prediction: "Attn" # CTC | Attn

    Training configuration

    training:

    numiter: 300000 # total training iterations

    batchsize: 192

    valInterval: 2000 # validation every 2000 iterations

    savedmodel: "./savedmodels" # directory for saving checkpoints

    FT: False # False = train from scratch, True = finetune

    continuemodel: "" # path to pretrained model (if FT=True)

    adam: False # False = Adadelta, True = Adam

    lr: 1.0 # learning rate

    beta1: 0.9 # for Adam

    rho: 0.95 # for Adadelta

    eps: 1e-8

    gradclip: 5 # gradient clipping

    numgpu: 1 # number of GPUs

    Augmentation

    augmentation:

    useaugmentation: True

    rotation: 5 # random rotation degree

    scalefactor: 0.1 # random scale

    blurprob: 0.3

    noiseprob: 0.3

    3. Character Set Configuration

    IMPORTANT: Adjust character set according to your dataset needs.

    Examples for various cases:

    Numbers only (digit recognition):
    character = "0123456789"
    

    Alphanumeric + basic punctuation:
    character = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!.,- "
    

    Full ASCII:
    import string
    

    character = string.printable[:-5] # exclude \t\n\r\v\f

    Character Set Tips:
    • Include ALL characters appearing in dataset
    • Don't include characters not in dataset
    • Case sensitive: 'A' != 'a'
    • Space character important for text with spaces

    Training Process

    1. Training from Scratch

    Train new model from zero (without pretrained weights):

    python train.py \
    

    --traindata dataset/lmdb/train \

    --validdata dataset/lmdb/validation \

    --selectdata train \

    --batchratio 1.0 \

    --Transformation TPS \

    --FeatureExtraction ResNet \

    --SequenceModeling BiLSTM \

    --Prediction Attn \

    --batchsize 192 \

    --numiter 300000 \

    --valInterval 2000 \

    --savedmodel ./savedmodels/ \

    --lr 1.0 \

    --workers 4 \

    --manualSeed 1111 \

    --rgb False

    Parameter explanation:
    • --traindata: Path to LMDB training dataset
    • --validdata: Path to LMDB validation dataset
    • --Transformation TPS: Use Thin Plate Spline to handle distortion
    • --FeatureExtraction ResNet: Use ResNet for feature extraction
    • --SequenceModeling BiLSTM: Bidirectional LSTM for sequence modeling
    • --Prediction Attn: Attention mechanism for prediction
    • --batchsize 192: Batch size (reduce if OOM)
    • --numiter 300000: Total training iterations
    • --valInterval 2000: Validation every 2000 iterations
    • --lr 1.0: Learning rate (for Adadelta)
    • --workers 4: Number of data loader workers
    • --rgb False: Grayscale images

    2. Finetuning from Pretrained Model

    Finetune from pretrained EasyOCR model to accelerate training:

    Download pretrained model:
    # Latin alphabet model (English)
    

    wget https://github.com/JaidedAI/EasyOCR/releases/download/v1.3/lating2.zip

    unzip lating2.zip

    Or other language models

    Indonesian

    wget https://github.com/JaidedAI/EasyOCR/releases/download/v1.3/indonesiang2.zip

    Finetune command:
    python train.py \
    

    --traindata dataset/lmdb/train \

    --validdata dataset/lmdb/validation \

    --selectdata train \

    --batchratio 1.0 \

    --Transformation TPS \

    --FeatureExtraction ResNet \

    --SequenceModeling BiLSTM \

    --Prediction Attn \

    --batchsize 192 \

    --numiter 50000 \

    --valInterval 1000 \

    --savedmodel ./finetunedmodels/ \

    --lr 0.0001 \

    --workers 4 \

    --manualSeed 1111 \

    --savedmodel lating2.pth \

    --FT

    Important differences when finetuning:
    • --savedmodel lating2.pth: Path to pretrained model
    • --FT: Flag to enable finetuning mode
    • --lr 0.0001: SMALLER learning rate (1000x smaller)
    • --numiter 50000: FEWER iterations (6x fewer)

    3. Monitoring Training with TensorBoard

    EasyOCR automatically logs metrics to TensorBoard. Open in new terminal:

    tensorboard --logdir=./savedmodels/
    

    Open browser: http://localhost:6006

    Metrics to monitor:
  • Training Loss: Should decrease consistently
    • Good: Smooth decrease
    • Bad: Plateau or increasing
    • Solution if bad: Reduce learning rate

  • Validation Accuracy: Target > 95% for quality dataset
    • Monitor overfitting: if train acc increases but val acc decreases
    • Solution: Early stopping, regularization, or augmentation

  • Character Error Rate (CER): Lower is better
    • Good: < 0.05 (5% error)
    • Acceptable: 0.05 - 0.15
    • Bad: > 0.15

  • Learning Rate Schedule: Track whether LR is optimal
  • Testing and Model Evaluation

    1. Testing Model with EasyOCR Reader

    After training completes, test model with script testmodel.py:

    import easyocr
    

    import cv2

    import os

    from pathlib import Path

    def testcustommodel(modelpath, testimagepath):

    """

    Test custom trained model

    """

    # Initialize reader with custom model

    reader = easyocr.Reader(

    ['en'], # Language (adjust)

    gpu=True,

    modelstoragedirectory='./models/',

    usernetworkdirectory=modelpath,

    recognetwork='custom'

    )

    # Read image

    result = reader.readtext(testimagepath, detail=1)

    # Print results

    print(f"\nResults for: {testimagepath}")

    print("-" 60)

    for bbox, text, confidence in result:

    print(f"Text: {text}")

    print(f"Confidence: {confidence:.4f}")

    print(f"BBox: {bbox}")

    print()

    return result

    def visualizeresults(imagepath, result, outputpath):

    """

    Visualize OCR results on image

    """

    img = cv2.imread(imagepath)

    for bbox, text, conf in result:

    # Extract coordinates

    topleft = tuple(map(int, bbox[0]))

    bottomright = tuple(map(int, bbox[2]))

    # Draw rectangle

    color = (0, 255, 0) if conf > 0.8 else (0, 165, 255) # Green if confident, Orange if not

    cv2.rectangle(img, topleft, bottomright, color, 2)

    # Draw text

    cv2.putText(

    img,

    f"{text} ({conf:.2f})",

    (topleft[0], topleft[1] - 10),

    cv2.FONTHERSHEYSIMPLEX,

    0.6,

    color,

    2

    )

    # Save result

    cv2.imwrite(outputpath, img)

    print(f"Visualization saved to: {outputpath}")

    Test on single image

    if name == "main":

    modelpath = "./savedmodels/bestaccuracy.pth"

    testimage = "dataset/test/sample001.jpg"

    result = testcustommodel(modelpath, testimage)

    visualizeresults(testimage, result, "outputvisualized.jpg")

    2. Comprehensive Evaluation on Test Set

    Create script evaluatemodel.py for thorough evaluation:

    import easyocr
    

    import cv2

    import os

    from tqdm import tqdm

    import Levenshtein

    import numpy as np

    import json

    def calculatemetrics(predicted, groundtruth):

    """

    Calculate CER, WER, and exact match accuracy

    """

    # Character Error Rate

    cer = Levenshtein.distance(predicted, groundtruth) / max(len(groundtruth), 1)

    # Word Error Rate

    predwords = predicted.split()

    gtwords = groundtruth.split()

    wer = Levenshtein.distance(' '.join(predwords), ' '.join(gtwords)) / max(len(gtwords), 1)

    # Exact match

    exactmatch = 1.0 if predicted == groundtruth else 0.0

    return {

    'cer': cer,

    'wer': wer,

    'exactmatch': exactmatch

    }

    def evaluateondataset(modelpath, testdatadir, labelfile):

    """

    Evaluate model on test dataset

    """

    # Initialize reader

    print("Loading model...")

    reader = easyocr.Reader(

    ['en'],

    gpu=True,

    modelstoragedirectory='./models/',

    usernetworkdirectory=modelpath,

    recognetwork='custom'

    )

    # Load test labels

    with open(labelfile, 'r', encoding='utf-8') as f:

    testsamples = [line.strip().split('\t') for line in f.readlines()]

    results = []

    totalcer = 0

    totalwer = 0

    totalexact = 0

    errors = []

    print(f"Evaluating on {len(testsamples)} samples...")

    for imgname, groundtruth in tqdm(testsamples):

    imgpath = os.path.join(testdatadir, imgname)

    if not os.path.exists(imgpath):

    print(f"Warning: Image not found - {imgpath}")

    continue

    # Predict

    try:

    result = reader.readtext(imgpath, detail=0)

    predicted = ' '.join(result) if result else ""

    except Exception as e:

    print(f"Error processing {imgname}: {e}")

    predicted = ""

    # Calculate metrics

    metrics = calculatemetrics(predicted, groundtruth)

    # Store results

    results.append({

    'image': imgname,

    'groundtruth': groundtruth,

    'predicted': predicted,

    *metrics

    })

    totalcer += metrics['cer']

    totalwer += metrics['wer']

    totalexact += metrics['exactmatch']

    # Track significant errors

    if metrics['cer'] > 0.3: # CER > 30%

    errors.append({

    'image': imgname,

    'groundtruth': groundtruth,

    'predicted': predicted,

    'cer': metrics['cer']

    })

    # Calculate averages

    nsamples = len(results)

    avgmetrics = {

    'avgcer': totalcer / nsamples,

    'avgwer': totalwer / nsamples,

    'exactmatchaccuracy': totalexact / nsamples,

    'totalsamples': nsamples

    }

    # Print summary

    print("\n" + "="60)

    print("EVALUATION RESULTS")

    print("="60)

    print(f"Total samples: {nsamples}")

    print(f"Average CER: {avgmetrics['avgcer']:.4f} ({avgmetrics['avgcer']100:.2f}%)")

    print(f"Average WER: {avgmetrics['avgwer']:.4f} ({avgmetrics['avgwer']100:.2f}%)")

    print(f"Exact Match Accuracy: {avgmetrics['exactmatchaccuracy']:.4f} ({avgmetrics['exactmatchaccuracy']100:.2f}%)")

    print(f"Significant errors (CER > 30%): {len(errors)}")

    # CER Distribution

    cervalues = [r['cer'] for r in results]

    print("\nCER Distribution:")

    print(f" Min: {min(cervalues):.4f}")

    print(f" Max: {max(cervalues):.4f}")

    print(f" Median: {np.median(cervalues):.4f}")

    print(f" Std Dev: {np.std(cervalues):.4f}")

    # Show worst errors

    if errors:

    print("\nTop 5 worst predictions:")

    errors.sort(key=lambda x: x['cer'], reverse=True)

    for i, err in enumerate(errors[:5], 1):

    print(f"\n{i}. {err['image']} (CER: {err['cer']:.4f})")

    print(f" GT: {err['groundtruth']}")

    print(f" Pred: {err['predicted']}")

    # Save detailed results

    with open('evaluationresults.json', 'w', encoding='utf-8') as f:

    json.dump({

    'summary': avgmetrics,

    'detailedresults': results,

    'errors': errors

    }, f, indent=2, ensureascii=False)

    print(f"\n✅ Detailed results saved to: evaluationresults.json")

    return avgmetrics, results

    if name == "main":

    modelpath = "./savedmodels/bestaccuracy.pth"

    testdatadir = "dataset/raw/test"

    labelfile = "dataset/labels/testlabels.txt"

    evaluateondataset(modelpath, testdatadir, labelfile)

    Run evaluation:

    python evaluatemodel.py
    

    Troubleshooting Common Issues

    1. Out of Memory (OOM)

    Symptom:
    CUDA out of memory` error Solutions:
    # Reduce batch size
    

    --batchsize 96 # instead of 192

    Gradient accumulation

    --gradaccumulationsteps 2 # Simulate larger batch

    Reduce image size

    --imgH 32 --imgW 128 # instead of 64x256

    Use gradient checkpointing

    model.gradientcheckpointingenable()

    2. Training Loss Not Decreasing

    Symptom: Loss stays constant or decreases very slowly Diagnosis & Solutions:
    # 1. Check learning rate
    

    - Too small: loss decreases very slowly

    - Too large: loss doesn't converge or increases

    Solution: LR finder

    def findlr(model, trainloader, startlr=1e-7, endlr=10, numiter=100):

    lrs = []

    losses = []

    optimizer = torch.optim.Adam(model.parameters(), lr=startlr)

    lrscheduler = torch.optim.lrscheduler.ExponentialLR(

    optimizer, gamma=(endlr/startlr)(1/numiter)

    )

    for batch in trainloader:

    loss = trainstep(model, batch)

    losses.append(loss.item())

    lrs.append(optimizer.paramgroups[0]['lr'])

    loss.backward()

    optimizer.step()

    lrscheduler.step()

    optimizer.zerograd()

    if len(lrs) >= numiter:

    break

    # Plot and choose LR at steepest descent

    import matplotlib.pyplot as plt

    plt.plot(lrs, losses)

    plt.xscale('log')

    plt.xlabel('Learning Rate')

    plt.ylabel('Loss')

    plt.show()

    2. Check data

    - Verify labels are correct

    - Check image quality

    - Ensure character set is complete

    3. Check model architecture

    - Ensure output dim = len(characterset)

    - Check model weights initialized properly

    3. Overfitting (High Train Acc, Low Val Acc)

    Symptom: Training accuracy high but validation accuracy low

    Solutions:

    # 1. More augmentation
    

    useaugmentation = True

    2. Dropout

    model = Model(dropout=0.3) # Add dropout layers

    3. Early stopping

    class EarlyStopping:

    def init(self, patience=10, mindelta=0.001):

    self.patience = patience

    self.mindelta = mindelta

    self.counter = 0

    self.bestvalloss = float('inf')

    def call(self, valloss):

    if valloss < self.bestvalloss - self.mindelta:

    self.bestvalloss = valloss

    self.counter = 0

    return False

    else:

    self.counter += 1

    if self.counter >= self.patience:

    print("Early stopping triggered!")

    return True

    return False

    4. Regularization

    optimizer = torch.optim.AdamW(

    model.parameters(),

    lr=0.001,

    weightdecay=0.01 # L2 regularization

    )

    5. More training data

    - Data augmentation

    - Synthetic data generation

    - Data collection

    4. Inference Too Slow

    Symptom: Model accurate but inference slow for production

    Solutions:

    # 1. Model quantization
    

    import torch.quantization as quantization

    Post-training quantization

    model.eval()

    modelquantized = quantization.quantizedynamic(

    model,

    {torch.nn.Linear, torch.nn.Conv2d},

    dtype=torch.qint8

    )

    2. ONNX export for inference optimization

    torch.onnx.export(

    model,

    dummyinput,

    "model.onnx",

    opsetversion=13,

    inputnames=['input'],

    outputnames=['output'],

    dynamicaxes={'input': {0: 'batchsize'}}

    )

    3. TensorRT (for NVIDIA GPUs)

    Significant speedup for production

    4. Batch inference

    Process multiple images simultaneously

    results = reader.readtextbatched(imagelist, batchsize=32)

    Best Practices and Recommendations

    1. Dataset Preparation

    Recommended dataset size:

    • Minimum: 1,000 samples
    • Good: 5,000 - 10,000 samples
    • Excellent: 50,000+ samples

    Split ratio:

    • Training: 80%
    • Validation: 10%
    • Test: 10%

    Data quality checklist:

    • [ ] All images readable and clear
    • [ ] Labels 100% accurate (manual audit)
    • [ ] Diverse fonts, sizes, styles
    • [ ] Various backgrounds and lighting
    • [ ] Balanced character distribution
    • [ ] No duplicate images

    2. Training Strategy

    For small dataset (< 5k samples):

    • Use pretrained model + finetuning
    • Heavy augmentation
    • Small learning rate (1e-4 to 1e-5)
    • Monitor overfitting closely
    • Early stopping with patience=10

    For large dataset (> 50k samples):

    • Train from scratch OK
    • Moderate augmentation
    • Standard learning rate (1.0 for Adadelta)
    • Longer training (300k iterations)
    • Less prone to overfitting

    3. Monitoring and Checkpointing

    # Save checkpoint strategy
    

    if iteration % saveinterval == 0:

    # Save checkpoint

    checkpoint = {

    'iteration': iteration,

    'modelstatedict': model.statedict(),

    'optimizerstatedict': optimizer.statedict(),

    'trainloss': trainloss,

    'valaccuracy': valaccuracy,

    }

    torch.save(checkpoint, f'checkpointiter{iteration}.pth')

    Keep best model

    if valaccuracy > bestvalaccuracy:

    bestvalaccuracy = valaccuracy

    torch.save(model.statedict(), 'bestaccuracy.pth')

    if valloss < bestvalloss:

    bestvalloss = valloss

    torch.save(model.statedict(), 'bestloss.pth')

    4. Hyperparameter Tuning Priority

    Order of importance for tuning:

  • Learning Rate (most important)
    • Start: 1.0 (Adadelta) or 1e-3 (Adam)
    • Tune with LR finder

  • Batch Size
    • Larger = more stable but needs memory
    • Typical: 64-256

  • Architecture
    • TPS + ResNet + BiLSTM + Attn (recommended)
    • Experiment with VGG or RCNN

  • Image Size
    • Default: 64x256
    • Larger for better text detail

  • Augmentation Strength
    • Start conservative, increase if overfitting

    Conclusion

    This tutorial covers the complete pipeline for finetuning EasyOCR:

    Key Takeaways:

  • Dataset is key: Invest time in high-quality dataset
  • Start with finetuning: Converges faster than training from scratch
  • Monitor closely: Use TensorBoard and validation metrics
  • Iterate: Experiment with hyperparameters and architecture
  • Test thoroughly: Evaluate on real-world data before deployment
  • Typical Timeline:

    • Dataset preparation: 2-5 days
    • Initial training: 1-3 days (GPU dependent)
    • Hyperparameter tuning: 2-7 days
    • Testing and evaluation: 1-2 days

    Total: 1-2 weeks** for production-ready model

    Next Steps:

    After model is trained:

    Related Articles

    Doctr Tutorial: Deep Learning OCR Library for Document Understanding

    Tutorial Doctr: Library OCR Deep Learning untuk Pemahaman Dokumen Doctr (Document Text Recognition) adalah library open-...

    Surya OCR Tutorial: Document AI for Text Detection, OCR, and Layout Analysis

    Tutorial Surya OCR: Document AI untuk Text Detection, OCR, dan Layout Analysis Surya adalah library Document AI open-sou...

    OpenCV + Deep Learning Tutorial: Modern Image Processing with Python

    OpenCV + Deep Learning: Tutorial Komprehensif Daftar Isi Pendahuluan Prasyarat Dasar-Dasar Preprocessing Gambar [T...

    Complete Ultralytics Tutorial: Object Detection with YOLO

    Tutorial Lengkap Ultralytics: Object Detection dengan YOLO Ultralytics adalah framework Python yang menyediakan implemen...