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:
trainlabels.txt:img001.jpg Hello World
img
002.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
- 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:
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
- Good: Smooth decrease
- Bad: Plateau or increasing
- Solution if bad: Reduce learning rate
- Monitor overfitting: if train acc increases but val acc decreases
- Solution: Early stopping, regularization, or augmentation
- Good: < 0.05 (5% error)
- Acceptable: 0.05 - 0.15
- Bad: > 0.15
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:
- Start: 1.0 (Adadelta) or 1e-3 (Adam)
- Tune with LR finder
- Larger = more stable but needs memory
- Typical: 64-256
- TPS + ResNet + BiLSTM + Attn (recommended)
- Experiment with VGG or RCNN
- Default: 64x256
- Larger for better text detail
- Start conservative, increase if overfitting
Conclusion
This tutorial covers the complete pipeline for finetuning EasyOCR:
Key Takeaways:
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: