Hausa-OCR-AJAMI / training_source.py
IntelligenceResearchLab's picture
Publish trained V10 from GitHub ee962f27a786
ebe8b54 verified
Raw History Blame Contribute Delete
48.7 kB
import os
import json
import csv
import time
import random
import unicodedata
from PIL import Image, ImageOps, ImageEnhance, ImageFilter
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, Sampler
# ============================================================
# CONFIGURATION
# ============================================================
TRAIN_DIR = (
r"C:\ai-test\OCR-AJAMI-Hausa"
r"\Hausa_repo_nouveau\data\train"
)
TEST_DIR = (
r"C:\ai-test\OCR-AJAMI-Hausa"
r"\Hausa_repo_nouveau\data\test"
)
SPLIT_PATH = (
r"C:\ai-test\hausa_split_nouveau_92_3_5.json"
)
VOCAB_PATH = r"C:\ai-test\hausa_ctc_vocab_nouveau.json"
OUTPUT_DIR = (
r"C:\ai-test\ctc_v10_nouveau_92_3_5"
)
BEST_CER_PATH = os.path.join(
OUTPUT_DIR,
"best_cer_v10.pt"
)
BEST_WER_PATH = os.path.join(
OUTPUT_DIR,
"best_wer_v10.pt"
)
LAST_PATH = os.path.join(
OUTPUT_DIR,
"last_v10.pt"
)
HISTORY_PATH = os.path.join(
OUTPUT_DIR,
"history_v10.csv"
)
# ============================================================
# PARAMETRES PRINCIPAUX
# ============================================================
HEIGHT = 96
BATCH_SIZE = 24
BUCKET_SIZE = 192
# ------------------------------------------------------------
# Chaque image réelle produit :
#
# vue 0 = originale
# vues 1-7 = transformations fixes
#
# Donc :
#
# TRAIN virtuel = TRAIN réel × 8
# ------------------------------------------------------------
VIEWS_PER_IMAGE = 8
# ------------------------------------------------------------
# ATTENTION :
#
# Un epoch V10 correspond à 8 passages virtuels par image.
#
# On ne fait donc pas 100 epochs.
# ------------------------------------------------------------
MAX_EPOCHS = 25
INITIAL_LR = 1e-3
WEIGHT_DECAY = 1e-4
NUM_WORKERS = 0
SEED = 42
VRAM_LIMIT_MB = 5500
# ============================================================
# EARLY STOP / SATURATION
# ============================================================
MIN_EPOCHS = 8
PATIENCE = 6
MIN_DELTA = 0.0005
TREND_WINDOW = 5
SLOPE_THRESHOLD = 0.0008
WINDOW_GAIN_THRESHOLD = 0.008
# ============================================================
# DEVICE
# ============================================================
DEVICE = torch.device(
"cuda"
if torch.cuda.is_available()
else "cpu"
)
os.makedirs(
OUTPUT_DIR,
exist_ok=True
)
# ============================================================
# REPRODUCTIBILITE
# ============================================================
random.seed(
SEED
)
torch.manual_seed(
SEED
)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(
SEED
)
torch.backends.cudnn.benchmark = True
# ============================================================
# VOCABULAIRE
# ============================================================
with open(
VOCAB_PATH,
"r",
encoding="utf-8"
) as f:
vocab = json.load(f)
BLANK_ID = vocab[
"<BLANK>"
]
char_to_id = {
c: i
for c, i in vocab.items()
if c != "<BLANK>"
}
id_to_char = {
i: c
for c, i in char_to_id.items()
}
NUM_CLASSES = len(
vocab
)
# ============================================================
# CHARGEMENT DU SPLIT 92 / 3 / 5
# ============================================================
with open(
SPLIT_PATH,
"r",
encoding="utf-8"
) as f:
split = json.load(f)
train_entries = split[
"train"
]
val_entries = split[
"validation"
]
test_entries = split[
"test"
]
# ============================================================
# VERIFICATION DU SPLIT
# ============================================================
TOTAL_SPLIT = (
len(train_entries)
+ len(val_entries)
+ len(test_entries)
)
if TOTAL_SPLIT == 0:
raise RuntimeError(
"Le split est vide."
)
train_ratio_real = (
len(train_entries)
/ TOTAL_SPLIT
)
val_ratio_real = (
len(val_entries)
/ TOTAL_SPLIT
)
test_ratio_real = (
len(test_entries)
/ TOTAL_SPLIT
)
print("=" * 72)
print("SPLIT CHARGE")
print("=" * 72)
print(
"TRAIN :",
len(train_entries),
f"({train_ratio_real * 100:.2f} %)"
)
print(
"VALIDATION :",
len(val_entries),
f"({val_ratio_real * 100:.2f} %)"
)
print(
"TEST :",
len(test_entries),
f"({test_ratio_real * 100:.2f} %)"
)
print(
"TOTAL :",
TOTAL_SPLIT
)
print("=" * 72)
# ============================================================
# RESOLUTION DES CHEMINS
# ============================================================
def resolve_path(
source,
filename
):
if source == "new_train":
return os.path.join(
TRAIN_DIR,
filename
)
if source == "new_test":
return os.path.join(
TEST_DIR,
filename
)
raise ValueError(
"Source inconnue : "
+ str(source)
)
# ============================================================
# AUGMENTATIONS FIXES
#
# 0 = originale
# 1 = luminosite / contraste
# 2 = epaisseur encre
# 3 = simulation scanner
# 4 = leger flou
# 5 = rotation
# 6 = deformation largeur/hauteur
# 7 = combinaison moderee
#
# IMPORTANT :
#
# Ces transformations sont DETERMINISTES.
#
# Une vue donnée reste identique à chaque epoch.
# ============================================================
def fixed_augmentation(
image,
variant,
sample_index
):
if variant == 0:
return image
rng = random.Random(
SEED
+ sample_index * 10007
+ variant * 1000003
)
original_w, original_h = (
image.size
)
# ========================================================
# VUE 1
#
# Papier / luminosite / contraste
# ========================================================
if variant == 1:
image = (
ImageEnhance.Brightness(
image
).enhance(
rng.uniform(
0.88,
1.10
)
)
)
image = (
ImageEnhance.Contrast(
image
).enhance(
rng.uniform(
0.82,
1.18
)
)
)
return image
# ========================================================
# VUE 2
#
# Epaisseur d'encre
# ========================================================
if variant == 2:
if rng.random() < 0.5:
image = image.filter(
ImageFilter.MinFilter(
3
)
)
else:
image = image.filter(
ImageFilter.MaxFilter(
3
)
)
image = (
ImageEnhance.Contrast(
image
).enhance(
rng.uniform(
0.92,
1.10
)
)
)
return image
# ========================================================
# VUE 3
#
# Simulation scanner
# ========================================================
if variant == 3:
scale = rng.uniform(
0.72,
0.90
)
w2 = max(
8,
round(
original_w
* scale
)
)
h2 = max(
8,
round(
original_h
* scale
)
)
image = image.resize(
(
w2,
h2
),
Image.Resampling.BILINEAR
)
image = image.resize(
(
original_w,
original_h
),
Image.Resampling.BICUBIC
)
return image
# ========================================================
# VUE 4
#
# Leger flou optique
# ========================================================
if variant == 4:
image = image.filter(
ImageFilter.GaussianBlur(
radius=rng.uniform(
0.20,
0.55
)
)
)
image = (
ImageEnhance.Contrast(
image
).enhance(
rng.uniform(
0.94,
1.08
)
)
)
return image
# ========================================================
# VUE 5
#
# Rotation legere
# ========================================================
if variant == 5:
angle = rng.uniform(
-1.0,
1.0
)
image = image.rotate(
angle,
resample=Image.Resampling.BICUBIC,
expand=True,
fillcolor=255
)
return image
# ========================================================
# VUE 6
#
# Variation geometrie
# ========================================================
if variant == 6:
sx = rng.uniform(
0.94,
1.06
)
sy = rng.uniform(
0.96,
1.04
)
new_w = max(
8,
round(
original_w
* sx
)
)
new_h = max(
8,
round(
original_h
* sy
)
)
image = image.resize(
(
new_w,
new_h
),
Image.Resampling.BICUBIC
)
return image
# ========================================================
# VUE 7
#
# Document réel / combinaison moderee
# ========================================================
if variant == 7:
image = (
ImageEnhance.Brightness(
image
).enhance(
rng.uniform(
0.92,
1.07
)
)
)
image = (
ImageEnhance.Contrast(
image
).enhance(
rng.uniform(
0.88,
1.14
)
)
)
if rng.random() < 0.50:
image = image.filter(
ImageFilter.GaussianBlur(
radius=rng.uniform(
0.12,
0.35
)
)
)
angle = rng.uniform(
-0.60,
0.60
)
image = image.rotate(
angle,
resample=Image.Resampling.BICUBIC,
expand=True,
fillcolor=255
)
return image
return image
# ============================================================
# DATASET TRAIN VIRTUEL
# ============================================================
class FixedTrainDataset(
Dataset
):
def __init__(
self,
entries
):
self.base_samples = []
self.expected_widths = []
for item in entries:
source = item[
"source"
]
filename = item[
"file_name"
]
text = unicodedata.normalize(
"NFC",
item[
"transcript"
]
)
path = resolve_path(
source,
filename
)
if not os.path.exists(
path
):
raise FileNotFoundError(
path
)
unknown = [
c
for c in text
if c not in char_to_id
]
if unknown:
raise RuntimeError(
f"Caracteres inconnus "
f"dans {filename}: "
f"{unknown}"
)
with Image.open(
path
) as img:
w, h = img.size
expected_width = max(
4,
round(
w
* HEIGHT
/ h
)
)
self.base_samples.append(
(
path,
text,
filename,
source,
expected_width
)
)
# ====================================================
# Largeurs approx pour bucketing
# ====================================================
for base_index in range(
len(self.base_samples)
):
expected = (
self.base_samples[
base_index
][4]
)
for _ in range(
VIEWS_PER_IMAGE
):
self.expected_widths.append(
expected
)
def __len__(
self
):
return (
len(
self.base_samples
)
* VIEWS_PER_IMAGE
)
def __getitem__(
self,
index
):
base_index = (
index
// VIEWS_PER_IMAGE
)
variant = (
index
% VIEWS_PER_IMAGE
)
(
path,
text,
filename,
source,
_
) = self.base_samples[
base_index
]
image = Image.open(
path
).convert(
"L"
)
image = fixed_augmentation(
image,
variant,
base_index
)
# ====================================================
# REDIMENSIONNEMENT
# ====================================================
w, h = image.size
new_w = max(
4,
round(
w
* HEIGHT
/ h
)
)
image = image.resize(
(
new_w,
HEIGHT
),
Image.Resampling.LANCZOS
)
# ====================================================
# RTL AJAMI -> LTR CTC
# ====================================================
image = ImageOps.mirror(
image
)
# ====================================================
# TENSOR
# ====================================================
data = torch.frombuffer(
bytearray(
image.tobytes()
),
dtype=torch.uint8
)
data = data.reshape(
HEIGHT,
new_w
).float()
data /= 255.0
data = (
data
- 0.5
) / 0.5
data = data.unsqueeze(
0
)
target = torch.tensor(
[
char_to_id[c]
for c in text
],
dtype=torch.long
)
return (
data,
target,
text,
new_w,
filename,
source,
variant
)
# ============================================================
# DATASET VAL / TEST
#
# AUCUNE AUGMENTATION
# ============================================================
class CleanDataset(
Dataset
):
def __init__(
self,
entries
):
self.samples = []
for item in entries:
source = item[
"source"
]
filename = item[
"file_name"
]
text = unicodedata.normalize(
"NFC",
item[
"transcript"
]
)
path = resolve_path(
source,
filename
)
if not os.path.exists(
path
):
raise FileNotFoundError(
path
)
unknown = [
c
for c in text
if c not in char_to_id
]
if unknown:
raise RuntimeError(
f"Caracteres inconnus "
f"dans {filename}: "
f"{unknown}"
)
self.samples.append(
(
path,
text,
filename,
source
)
)
def __len__(
self
):
return len(
self.samples
)
def __getitem__(
self,
index
):
(
path,
text,
filename,
source
) = self.samples[
index
]
image = Image.open(
path
).convert(
"L"
)
w, h = image.size
new_w = max(
4,
round(
w
* HEIGHT
/ h
)
)
image = image.resize(
(
new_w,
HEIGHT
),
Image.Resampling.LANCZOS
)
image = ImageOps.mirror(
image
)
data = torch.frombuffer(
bytearray(
image.tobytes()
),
dtype=torch.uint8
)
data = data.reshape(
HEIGHT,
new_w
).float()
data /= 255.0
data = (
data
- 0.5
) / 0.5
data = data.unsqueeze(
0
)
target = torch.tensor(
[
char_to_id[c]
for c in text
],
dtype=torch.long
)
return (
data,
target,
text,
new_w,
filename,
source,
0
)
# ============================================================
# COLLATE
# ============================================================
def collate_fn(
batch
):
images = []
targets = []
texts = []
widths = []
filenames = []
sources = []
variants = []
for (
image,
target,
text,
width,
filename,
source,
variant
) in batch:
images.append(
image
)
targets.append(
target
)
texts.append(
text
)
widths.append(
width
)
filenames.append(
filename
)
sources.append(
source
)
variants.append(
variant
)
max_width = max(
image.shape[-1]
for image in images
)
padded = []
for image in images:
pad_width = (
max_width
- image.shape[-1]
)
image = F.pad(
image,
(
0,
pad_width,
0,
0
),
value=1.0
)
padded.append(
image
)
return (
torch.stack(
padded
),
torch.cat(
targets
),
torch.tensor(
[
len(t)
for t in targets
],
dtype=torch.long
),
texts,
torch.tensor(
widths,
dtype=torch.long
),
filenames,
sources,
variants
)
# ============================================================
# BUCKET SAMPLER
# ============================================================
class WidthBucketSampler(
Sampler
):
def __init__(
self,
dataset,
batch_size,
bucket_size,
seed
):
self.dataset = dataset
self.batch_size = batch_size
self.bucket_size = (
bucket_size
)
self.seed = seed
self.epoch = 0
def set_epoch(
self,
epoch
):
self.epoch = epoch
def __len__(
self
):
return (
len(self.dataset)
+ self.batch_size
- 1
) // self.batch_size
def __iter__(
self
):
rng = random.Random(
self.seed
+ self.epoch
)
indices = list(
range(
len(self.dataset)
)
)
rng.shuffle(
indices
)
batches = []
for start in range(
0,
len(indices),
self.bucket_size
):
bucket = indices[
start:
start
+ self.bucket_size
]
bucket.sort(
key=lambda idx:
self.dataset.expected_widths[
idx
]
)
for pos in range(
0,
len(bucket),
self.batch_size
):
batch = bucket[
pos:
pos
+ self.batch_size
]
if batch:
batches.append(
batch
)
rng.shuffle(
batches
)
for batch in batches:
yield batch
# ============================================================
# CRNN
#
# IDENTIQUE A V8 / V9 / V10 ORIGINAL
# ============================================================
class CRNN(
nn.Module
):
def __init__(
self,
num_classes
):
super().__init__()
self.cnn = nn.Sequential(
nn.Conv2d(
1,
64,
3,
padding=1
),
nn.BatchNorm2d(
64
),
nn.ReLU(
inplace=True
),
nn.MaxPool2d(
(
2,
2
)
),
nn.Conv2d(
64,
128,
3,
padding=1
),
nn.BatchNorm2d(
128
),
nn.ReLU(
inplace=True
),
nn.MaxPool2d(
(
2,
2
)
),
nn.Conv2d(
128,
256,
3,
padding=1
),
nn.BatchNorm2d(
256
),
nn.ReLU(
inplace=True
),
nn.MaxPool2d(
(
2,
1
)
),
nn.Conv2d(
256,
256,
3,
padding=1
),
nn.BatchNorm2d(
256
),
nn.ReLU(
inplace=True
),
nn.MaxPool2d(
(
2,
1
)
),
nn.Conv2d(
256,
384,
3,
padding=1
),
nn.BatchNorm2d(
384
),
nn.ReLU(
inplace=True
),
nn.MaxPool2d(
(
2,
1
)
),
nn.Conv2d(
384,
384,
3,
padding=1
),
nn.BatchNorm2d(
384
),
nn.ReLU(
inplace=True
),
nn.AdaptiveAvgPool2d(
(
1,
None
)
)
)
self.rnn = nn.LSTM(
input_size=384,
hidden_size=256,
num_layers=2,
bidirectional=True,
batch_first=True,
dropout=0.2
)
self.classifier = nn.Linear(
512,
num_classes
)
def forward(
self,
x
):
x = self.cnn(
x
)
x = x.squeeze(
2
)
x = x.permute(
0,
2,
1
)
x, _ = self.rnn(
x
)
return self.classifier(
x
)
# ============================================================
# CTC LENGTH
# ============================================================
def output_lengths(
widths
):
return (
widths
// 2
// 2
)
# ============================================================
# DECODAGE CTC GREEDY
# ============================================================
def decode_ctc(
ids
):
chars = []
previous = None
for idx in ids:
idx = int(
idx
)
if (
idx != BLANK_ID
and idx != previous
):
chars.append(
id_to_char.get(
idx,
""
)
)
previous = idx
return "".join(
chars
)
# ============================================================
# LEVENSHTEIN
# ============================================================
def edit_distance(
a,
b
):
previous = list(
range(
len(b)
+ 1
)
)
for i, aa in enumerate(
a,
1
):
current = [
i
]
for j, bb in enumerate(
b,
1
):
current.append(
min(
current[
j - 1
] + 1,
previous[
j
] + 1,
previous[
j - 1
]
+ (
aa != bb
)
)
)
previous = current
return previous[
-1
]
# ============================================================
# EVALUATION
# ============================================================
@torch.no_grad()
def evaluate(
model,
loader,
max_examples=5
):
model.eval()
char_errors = 0
char_total = 0
word_errors = 0
word_total = 0
exact = 0
total_lines = 0
examples = []
for batch in loader:
(
images,
targets,
target_lengths,
texts,
widths,
filenames,
sources,
variants
) = batch
images = images.to(
DEVICE,
non_blocking=True
)
logits = model(
images
)
predictions = logits.argmax(
dim=-1
).cpu()
lengths = output_lengths(
widths
)
for i, reference in enumerate(
texts
):
prediction = decode_ctc(
predictions[
i,
:int(
lengths[i]
)
]
)
reference = (
unicodedata.normalize(
"NFC",
reference
)
)
prediction = (
unicodedata.normalize(
"NFC",
prediction
)
)
char_errors += (
edit_distance(
reference,
prediction
)
)
char_total += len(
reference
)
ref_words = (
reference.split()
)
pred_words = (
prediction.split()
)
word_errors += (
edit_distance(
ref_words,
pred_words
)
)
word_total += len(
ref_words
)
if prediction == reference:
exact += 1
total_lines += 1
if (
len(examples)
< max_examples
):
examples.append(
(
filenames[i],
reference,
prediction
)
)
cer = (
char_errors
/ char_total
if char_total > 0
else 0.0
)
wer = (
word_errors
/ word_total
if word_total > 0
else 0.0
)
exact_rate = (
exact
/ total_lines
if total_lines > 0
else 0.0
)
return (
cer,
wer,
exact_rate,
examples
)
# ============================================================
# PENTE LINEAIRE
# ============================================================
def linear_slope(
values
):
n = len(
values
)
if n < 2:
return 0.0
x_mean = (
n - 1
) / 2.0
y_mean = (
sum(values)
/ n
)
numerator = 0.0
denominator = 0.0
for i, value in enumerate(
values
):
dx = (
i
- x_mean
)
numerator += (
dx
* (
value
- y_mean
)
)
denominator += (
dx
* dx
)
if denominator == 0:
return 0.0
return (
numerator
/ denominator
)
# ============================================================
# CHECKPOINT
# ============================================================
def save_checkpoint(
path,
epoch,
model,
optimizer,
val_cer,
val_wer,
val_exact
):
torch.save(
{
"epoch":
epoch,
"model_state_dict":
model.state_dict(),
"optimizer_state_dict":
optimizer.state_dict(),
"val_cer":
val_cer,
"val_wer":
val_wer,
"val_exact_rate":
val_exact,
"version":
"V10_NOUVEAU_92_3_5",
"height":
HEIGHT,
"batch_size":
BATCH_SIZE,
"base_train_images":
len(
train_entries
),
"views_per_image":
VIEWS_PER_IMAGE,
"virtual_train_size":
len(
train_entries
)
* VIEWS_PER_IMAGE,
"dynamic_augmentation":
False,
"fixed_augmentation":
True,
"rtl_mirror":
True,
"from_scratch":
True,
"split":
"92_3_5",
"split_path":
SPLIT_PATH,
"vocab":
vocab
},
path
)
# ============================================================
# DATASETS
# ============================================================
print()
print("=" * 72)
print("CREATION DATASET VIRTUEL V10")
print("=" * 72)
train_dataset = (
FixedTrainDataset(
train_entries
)
)
val_dataset = (
CleanDataset(
val_entries
)
)
test_dataset = (
CleanDataset(
test_entries
)
)
# ============================================================
# SAMPLER
# ============================================================
train_sampler = (
WidthBucketSampler(
train_dataset,
BATCH_SIZE,
BUCKET_SIZE,
SEED
)
)
# ============================================================
# LOADERS
# ============================================================
train_loader = DataLoader(
train_dataset,
batch_sampler=train_sampler,
num_workers=NUM_WORKERS,
collate_fn=collate_fn,
pin_memory=True
)
val_loader = DataLoader(
val_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=NUM_WORKERS,
collate_fn=collate_fn,
pin_memory=True
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=NUM_WORKERS,
collate_fn=collate_fn,
pin_memory=True
)
# ============================================================
# MODELE
# ============================================================
model = CRNN(
NUM_CLASSES
).to(
DEVICE
)
# ============================================================
# LOSS
# ============================================================
criterion = nn.CTCLoss(
blank=BLANK_ID,
reduction="mean",
zero_infinity=True
)
# ============================================================
# OPTIMIZER
# ============================================================
optimizer = torch.optim.AdamW(
model.parameters(),
lr=INITIAL_LR,
weight_decay=WEIGHT_DECAY
)
# ============================================================
# SCHEDULER
# ============================================================
scheduler = (
torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode="min",
factor=0.5,
patience=3,
min_lr=1e-6
)
)
# ============================================================
# INFO
# ============================================================
print()
print("=" * 72)
print("HAUSA CTC V10 - NOUVEAU DATASET 92/3/5")
print("=" * 72)
print(
"Device :",
DEVICE
)
if torch.cuda.is_available():
print(
"GPU :",
torch.cuda.get_device_name(
0
)
)
print(
"Train reel :",
len(train_entries)
)
print(
"Validation :",
len(val_entries)
)
print(
"Test :",
len(test_entries)
)
print(
"Vues / image train :",
VIEWS_PER_IMAGE
)
print(
"Train virtuel :",
len(train_dataset)
)
print(
"Height :",
HEIGHT
)
print(
"Batch :",
BATCH_SIZE
)
print(
"Augmentation fixe : ON"
)
print(
"Augmentation dynamique:",
"OFF"
)
print(
"Initialisation : FROM SCRATCH"
)
print(
"Epochs max :",
MAX_EPOCHS
)
print(
"LR initial :",
INITIAL_LR
)
print(
"VRAM limite :",
VRAM_LIMIT_MB,
"MB"
)
print("=" * 72)
# ============================================================
# HISTORIQUE CSV
# ============================================================
with open(
HISTORY_PATH,
"w",
newline="",
encoding="utf-8"
) as f:
writer = csv.writer(
f
)
writer.writerow(
[
"epoch",
"train_loss",
"val_cer",
"val_wer",
"val_exact",
"slope",
"window_gain",
"lr",
"seconds",
"vram_mb"
]
)
# ============================================================
# ETAT
# ============================================================
best_cer = float(
"inf"
)
best_wer = float(
"inf"
)
best_cer_epoch = 0
best_wer_epoch = 0
best_for_patience = float(
"inf"
)
without_improvement = 0
cer_history = []
start_total = time.time()
# ============================================================
# TRAIN
# ============================================================
for epoch in range(
1,
MAX_EPOCHS + 1
):
train_sampler.set_epoch(
epoch
)
model.train()
running_loss = 0.0
batches = 0
start_epoch = time.time()
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
# ========================================================
# BATCH LOOP
# ========================================================
for batch in train_loader:
(
images,
targets,
target_lengths,
texts,
widths,
filenames,
sources,
variants
) = batch
images = images.to(
DEVICE,
non_blocking=True
)
targets = targets.to(
DEVICE,
non_blocking=True
)
target_lengths = (
target_lengths.to(
DEVICE,
non_blocking=True
)
)
input_lengths = (
output_lengths(
widths
).to(
DEVICE
)
)
optimizer.zero_grad(
set_to_none=True
)
logits = model(
images
)
log_probs = F.log_softmax(
logits,
dim=-1
).permute(
1,
0,
2
)
loss = criterion(
log_probs,
targets,
input_lengths,
target_lengths
)
loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
5.0
)
optimizer.step()
running_loss += (
loss.item()
)
batches += 1
# ========================================================
# TRAIN LOSS
# ========================================================
train_loss = (
running_loss
/ max(
1,
batches
)
)
# ========================================================
# VALIDATION
# ========================================================
(
val_cer,
val_wer,
val_exact,
examples
) = evaluate(
model,
val_loader,
5
)
cer_history.append(
val_cer
)
# ========================================================
# SCHEDULER
# ========================================================
scheduler.step(
val_cer
)
lr = (
optimizer.param_groups[
0
]["lr"]
)
# ========================================================
# TENDANCE
# ========================================================
if (
len(cer_history)
>= TREND_WINDOW
):
recent = cer_history[
-TREND_WINDOW:
]
slope = linear_slope(
recent
)
window_gain = (
recent[0]
- min(
recent
)
)
else:
slope = 0.0
window_gain = float(
"inf"
)
# ========================================================
# BEST CER
# ========================================================
if val_cer < best_cer:
best_cer = val_cer
best_cer_epoch = (
epoch
)
save_checkpoint(
BEST_CER_PATH,
epoch,
model,
optimizer,
val_cer,
val_wer,
val_exact
)
print()
print(
">>> NOUVEAU BEST CER"
)
# ========================================================
# BEST WER
# ========================================================
if val_wer < best_wer:
best_wer = val_wer
best_wer_epoch = (
epoch
)
save_checkpoint(
BEST_WER_PATH,
epoch,
model,
optimizer,
val_cer,
val_wer,
val_exact
)
print()
print(
">>> NOUVEAU BEST WER"
)
# ========================================================
# PATIENCE
# ========================================================
if (
val_cer
< best_for_patience
- MIN_DELTA
):
best_for_patience = (
val_cer
)
without_improvement = 0
else:
without_improvement += 1
# ========================================================
# TEMPS
# ========================================================
seconds = (
time.time()
- start_epoch
)
# ========================================================
# VRAM
# ========================================================
if torch.cuda.is_available():
vram_mb = (
torch.cuda.max_memory_allocated()
/ 1024**2
)
else:
vram_mb = 0.0
# ========================================================
# DISPLAY
# ========================================================
print()
print("=" * 72)
print(
f"EPOCH {epoch:03d}/{MAX_EPOCHS}"
)
print("=" * 72)
print(
f"Loss train : {train_loss:.4f}"
)
print(
f"VAL CER : {val_cer:.4f}"
)
print(
f"VAL WER : {val_wer:.4f}"
)
print(
f"VAL exact : {val_exact:.4f}"
)
if (
len(cer_history)
>= TREND_WINDOW
):
print(
f"Pente CER : "
f"{slope:+.6f}/epoch"
)
print(
f"Gain fenetre: "
f"{window_gain:.4f}"
)
else:
print(
"Pente CER : attente"
)
print(
f"LR : {lr:.8f}"
)
print(
f"Temps : {seconds:.1f} sec"
)
print(
f"VRAM alloc : {vram_mb:.0f} MB"
)
print(
"Train reel :",
len(train_entries)
)
print(
"Vues epoch :",
len(train_dataset)
)
print(
"Equivalent :",
f"x{VIEWS_PER_IMAGE}"
)
if (
vram_mb
> VRAM_LIMIT_MB
):
print()
print(
"!!! ALERTE VRAM !!!"
)
# ========================================================
# EXEMPLES
# ========================================================
for (
filename,
reference,
prediction
) in examples:
print()
print(
"FILE:",
filename
)
print(
"REF :",
reference
)
print(
"PRED:",
prediction
)
# ========================================================
# BEST INFO
# ========================================================
print()
print(
"Best CER :",
round(
best_cer,
4
),
"@",
best_cer_epoch
)
print(
"Best WER :",
round(
best_wer,
4
),
"@",
best_wer_epoch
)
print(
"Sans gain :",
without_improvement,
"/",
PATIENCE
)
# ========================================================
# CSV
# ========================================================
with open(
HISTORY_PATH,
"a",
newline="",
encoding="utf-8"
) as f:
writer = csv.writer(
f
)
writer.writerow(
[
epoch,
train_loss,
val_cer,
val_wer,
val_exact,
slope,
window_gain,
lr,
seconds,
vram_mb
]
)
# ========================================================
# LAST
# ========================================================
save_checkpoint(
LAST_PATH,
epoch,
model,
optimizer,
val_cer,
val_wer,
val_exact
)
# ========================================================
# SATURATION
# ========================================================
saturation = (
epoch
>= MIN_EPOCHS
and
len(
cer_history
)
>= TREND_WINDOW
and
abs(
slope
)
< SLOPE_THRESHOLD
and
window_gain
< WINDOW_GAIN_THRESHOLD
and
without_improvement
>= PATIENCE
)
if saturation:
print()
print("=" * 72)
print(
"SATURATION DETECTEE"
)
print(
"Best epoch :",
best_cer_epoch
)
print(
"Best CER :",
round(
best_cer,
4
)
)
print(
"Pente :",
round(
slope,
6
)
)
print(
"Gain :",
round(
window_gain,
4
)
)
print("=" * 72)
break
# ============================================================
# TEST FINAL
#
# On recharge le BEST CER.
# ============================================================
print()
print("=" * 72)
print(
"TEST FINAL"
)
print("=" * 72)
checkpoint = torch.load(
BEST_CER_PATH,
map_location=DEVICE,
weights_only=False
)
model.load_state_dict(
checkpoint[
"model_state_dict"
]
)
(
test_cer,
test_wer,
test_exact,
examples
) = evaluate(
model,
test_loader,
10
)
minutes = (
time.time()
- start_total
) / 60
# ============================================================
# RESUME FINAL
# ============================================================
print()
print("=" * 72)
print(
"RESUME FINAL V10"
)
print("=" * 72)
print(
"BEST epoch :",
checkpoint[
"epoch"
]
)
print(
"BEST VAL CER :",
round(
checkpoint[
"val_cer"
],
4
)
)
print(
"BEST VAL WER :",
round(
checkpoint[
"val_wer"
],
4
)
)
print(
"BEST VAL exact :",
round(
checkpoint[
"val_exact_rate"
],
4
)
)
print()
print(
"TEST CER :",
round(
test_cer,
4
)
)
print(
"TEST WER :",
round(
test_wer,
4
)
)
print(
"TEST exact rate :",
round(
test_exact,
4
)
)
print()
print(
"Train réel :",
len(
train_entries
)
)
print(
"Train virtuel :",
len(
train_dataset
)
)
print(
"Vues / image :",
VIEWS_PER_IMAGE
)
print()
print(
"Temps total :",
round(
minutes,
1
),
"minutes"
)
print()
print(
"Best CER :",
BEST_CER_PATH
)
print(
"Best WER :",
BEST_WER_PATH
)
print(
"History :",
HISTORY_PATH
)
# ============================================================
# EXEMPLES TEST
# ============================================================
print()
print("=" * 72)
print(
"EXEMPLES TEST"
)
print("=" * 72)
for (
filename,
reference,
prediction
) in examples:
print()
print(
"FILE:",
filename
)
print(
"REF :",
reference
)
print(
"PRED:",
prediction
)