Compare commits
26 Commits
32b88aa9b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc66ac9520 | ||
|
|
8b45028101 | ||
|
|
ef4515adcc | ||
|
|
7aea840821 | ||
|
|
71e595a966 | ||
|
|
d5313fb143 | ||
|
|
51e114b1ee | ||
|
|
fa3ad61dd7 | ||
|
|
70e4932cd0 | ||
|
|
aee2716a41 | ||
|
|
bf7949d8c3 | ||
|
|
a4296d012e | ||
|
|
c5d372e98d | ||
|
|
8d2e5ac021 | ||
|
|
adca297850 | ||
|
|
83b2eccd07 | ||
|
|
8dfb2b81e0 | ||
|
|
4ed1ffa226 | ||
|
|
182e6e7a98 | ||
|
|
50f5bef7f1 | ||
|
|
9c3ac3f977 | ||
|
|
defcc38435 | ||
|
|
e9a090ad9d | ||
|
|
156220e62b | ||
|
|
89c2197a42 | ||
|
|
aa2ab7dbec |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,6 @@
|
|||||||
output_temp.txt
|
output_temp.txt
|
||||||
checkpoint.json
|
checkpoint.json
|
||||||
|
Traduction/Modelfile
|
||||||
|
.env
|
||||||
|
Traduction/*.pdf
|
||||||
|
Traduction/*.txt
|
||||||
|
|||||||
2
Finetunning/.gitignore
vendored
Normal file
2
Finetunning/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Les modèles générés
|
||||||
|
qwen2.5*/
|
||||||
33778
Finetunning/Paires-de-phrases-en-ukrainien-francais-2026-01-06.tsv
Normal file
33778
Finetunning/Paires-de-phrases-en-ukrainien-francais-2026-01-06.tsv
Normal file
File diff suppressed because it is too large
Load Diff
144
Finetunning/cleanDataSet.py
Normal file
144
Finetunning/cleanDataSet.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import json
|
||||||
|
import unicodedata
|
||||||
|
import re
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Configuration
|
||||||
|
# ----------------------------
|
||||||
|
INPUT_FILE = "paires.json"
|
||||||
|
OUTPUT_FILE = "paires_clean.json"
|
||||||
|
|
||||||
|
MIN_TOKENS = 5
|
||||||
|
MAX_TOKENS = 200
|
||||||
|
MIN_QUALITY_SCORE = 0.60
|
||||||
|
|
||||||
|
print("=== Dataset cleaning + quality scoring started ===")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Normalization helpers
|
||||||
|
# ----------------------------
|
||||||
|
def normalize_text(text: str) -> str:
|
||||||
|
text = unicodedata.normalize("NFKC", text)
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
text = text.replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def token_count(text: str) -> int:
|
||||||
|
return len(text.split())
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Quality scoring
|
||||||
|
# ----------------------------
|
||||||
|
def length_ratio_score(src_len, tgt_len):
|
||||||
|
"""
|
||||||
|
Ideal ratio FR/UK ≈ 0.9 – 1.3
|
||||||
|
"""
|
||||||
|
ratio = tgt_len / max(src_len, 1)
|
||||||
|
|
||||||
|
if ratio < 0.5 or ratio > 2.0:
|
||||||
|
return 0.0
|
||||||
|
elif 0.75 <= ratio <= 1.5:
|
||||||
|
return 1.0
|
||||||
|
else:
|
||||||
|
return max(0.0, 1.0 - abs(ratio - 1.1))
|
||||||
|
|
||||||
|
|
||||||
|
def lexical_density_score(text):
|
||||||
|
"""
|
||||||
|
Penalize very repetitive or trivial translations
|
||||||
|
"""
|
||||||
|
tokens = text.split()
|
||||||
|
if not tokens:
|
||||||
|
return 0.0
|
||||||
|
unique_ratio = len(set(tokens)) / len(tokens)
|
||||||
|
return min(1.0, unique_ratio * 1.5)
|
||||||
|
|
||||||
|
|
||||||
|
def quality_score(src, tgt):
|
||||||
|
src_len = token_count(src)
|
||||||
|
tgt_len = token_count(tgt)
|
||||||
|
|
||||||
|
l_score = length_ratio_score(src_len, tgt_len)
|
||||||
|
d_score = lexical_density_score(tgt)
|
||||||
|
|
||||||
|
return 0.7 * l_score + 0.3 * d_score
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load + clean + score
|
||||||
|
# ----------------------------
|
||||||
|
unique_sources = OrderedDict()
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"total": 0,
|
||||||
|
"removed_length": 0,
|
||||||
|
"removed_duplicates": 0,
|
||||||
|
"removed_quality": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(INPUT_FILE, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
stats["total"] += 1
|
||||||
|
item = json.loads(line)
|
||||||
|
|
||||||
|
src = normalize_text(item["text"])
|
||||||
|
tgt = normalize_text(item["translation"])
|
||||||
|
|
||||||
|
src_len = token_count(src)
|
||||||
|
tgt_len = token_count(tgt)
|
||||||
|
|
||||||
|
# Length filtering
|
||||||
|
if not (MIN_TOKENS <= src_len <= MAX_TOKENS):
|
||||||
|
stats["removed_length"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not (MIN_TOKENS <= tgt_len <= MAX_TOKENS):
|
||||||
|
stats["removed_length"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Deduplication
|
||||||
|
if src in unique_sources:
|
||||||
|
stats["removed_duplicates"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Quality score
|
||||||
|
q_score = quality_score(src, tgt)
|
||||||
|
if q_score < MIN_QUALITY_SCORE:
|
||||||
|
stats["removed_quality"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
unique_sources[src] = {
|
||||||
|
"translation": tgt,
|
||||||
|
"quality_score": round(q_score, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Report
|
||||||
|
# ----------------------------
|
||||||
|
print(f"Total lines processed: {stats['total']}")
|
||||||
|
print(f"Removed (length): {stats['removed_length']}")
|
||||||
|
print(f"Removed (duplicates): {stats['removed_duplicates']}")
|
||||||
|
print(f"Removed (quality): {stats['removed_quality']}")
|
||||||
|
print(f"Final kept pairs: {len(unique_sources)}")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Save cleaned dataset
|
||||||
|
# ----------------------------
|
||||||
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
|
for src, data in unique_sources.items():
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"text": src,
|
||||||
|
"translation": data["translation"],
|
||||||
|
"quality_score": data["quality_score"],
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
ensure_ascii=False
|
||||||
|
)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
print(f"=== Cleaning completed ===")
|
||||||
|
print(f"Clean dataset saved to: {OUTPUT_FILE}")
|
||||||
210
Finetunning/finetunning.py
Normal file
210
Finetunning/finetunning.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import os
|
||||||
|
import torch
|
||||||
|
from datasets import load_dataset
|
||||||
|
from transformers import (
|
||||||
|
AutoTokenizer,
|
||||||
|
AutoModelForCausalLM,
|
||||||
|
TrainingArguments,
|
||||||
|
BitsAndBytesConfig,
|
||||||
|
)
|
||||||
|
from peft import (
|
||||||
|
LoraConfig,
|
||||||
|
get_peft_model,
|
||||||
|
prepare_model_for_kbit_training,
|
||||||
|
)
|
||||||
|
from trl import SFTTrainer
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Environment safety (Windows + AMP fixes)
|
||||||
|
# ----------------------------
|
||||||
|
os.environ["TORCHDYNAMO_DISABLE"] = "1"
|
||||||
|
os.environ["ACCELERATE_MIXED_PRECISION"] = "no" # ✅ disable AMP completely
|
||||||
|
os.environ["TORCH_AMP_DISABLE"] = "1" # ✅ disable GradScaler
|
||||||
|
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # optional: force first GPU
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Global configuration
|
||||||
|
# ----------------------------
|
||||||
|
MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
|
||||||
|
OUTPUT_DIR = "./qwen2.5-7b-uk-fr-lora"
|
||||||
|
DATA_FILE = "paires_clean.json"
|
||||||
|
MAX_SEQ_LENGTH = 512 # Reduce for RTX 4080 SUPER VRAM
|
||||||
|
|
||||||
|
print(f"\n=== Starting fine-tuning script for {MODEL_NAME} ===\n")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [1/7] Tokenizer
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[1/7] Loading tokenizer...")
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
MODEL_NAME,
|
||||||
|
trust_remote_code=True,
|
||||||
|
)
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
tokenizer.model_max_length = MAX_SEQ_LENGTH
|
||||||
|
print("Tokenizer loaded.")
|
||||||
|
print(f"Pad token id: {tokenizer.pad_token_id}")
|
||||||
|
print(f"Max sequence length: {tokenizer.model_max_length}")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [2/7] Load model in 4-bit (QLoRA)
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[2/7] Loading model in 4-bit mode (QLoRA)...")
|
||||||
|
assert torch.cuda.is_available(), "CUDA GPU not detected!"
|
||||||
|
print(f"Using GPU: {torch.cuda.get_device_name(0)}")
|
||||||
|
|
||||||
|
bnb_config = BitsAndBytesConfig(
|
||||||
|
load_in_4bit=True,
|
||||||
|
bnb_4bit_quant_type="nf4",
|
||||||
|
bnb_4bit_compute_dtype=torch.float16, # fp16 internally
|
||||||
|
bnb_4bit_use_double_quant=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
MODEL_NAME,
|
||||||
|
device_map="auto",
|
||||||
|
quantization_config=bnb_config,
|
||||||
|
low_cpu_mem_usage=True,
|
||||||
|
trust_remote_code=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Align model tokens with tokenizer
|
||||||
|
model.config.pad_token_id = tokenizer.pad_token_id
|
||||||
|
model.config.bos_token_id = tokenizer.bos_token_id
|
||||||
|
model.config.eos_token_id = tokenizer.eos_token_id
|
||||||
|
print("Model loaded successfully in 4-bit mode on GPU.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [3/7] Prepare model for k-bit training
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[3/7] Preparing model for k-bit training...")
|
||||||
|
model = prepare_model_for_kbit_training(model)
|
||||||
|
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
||||||
|
model.config.use_cache = False # Important with gradient checkpointing + QLoRA
|
||||||
|
print("Model prepared for k-bit training.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [4/7] LoRA configuration
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[4/7] Configuring LoRA adapters...")
|
||||||
|
lora_config = LoraConfig(
|
||||||
|
r=32,
|
||||||
|
lora_alpha=64,
|
||||||
|
lora_dropout=0.02,
|
||||||
|
bias="none",
|
||||||
|
task_type="CAUSAL_LM",
|
||||||
|
target_modules=[
|
||||||
|
"q_proj",
|
||||||
|
"k_proj",
|
||||||
|
"v_proj",
|
||||||
|
"o_proj",
|
||||||
|
"gate_proj",
|
||||||
|
"up_proj",
|
||||||
|
"down_proj",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
model = get_peft_model(model, lora_config)
|
||||||
|
model.print_trainable_parameters()
|
||||||
|
print("LoRA adapters successfully attached.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [5/7] Dataset loading & formatting
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[5/7] Loading dataset from JSON file...")
|
||||||
|
dataset = load_dataset("json", data_files=DATA_FILE)
|
||||||
|
print(f"Dataset loaded with {len(dataset['train'])} samples.")
|
||||||
|
|
||||||
|
print("Formatting dataset for Ukrainian → French translation...")
|
||||||
|
def format_prompt(example):
|
||||||
|
return {
|
||||||
|
"text": (
|
||||||
|
"<|im_start|>user\n"
|
||||||
|
"Translate the following Ukrainian text into French.\n"
|
||||||
|
f"Ukrainian: {example['text']}\n"
|
||||||
|
"<|im_end|>\n"
|
||||||
|
"<|im_start|>assistant\n"
|
||||||
|
f"{example['translation']}"
|
||||||
|
"<|im_end|>"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataset = dataset.map(
|
||||||
|
format_prompt,
|
||||||
|
remove_columns=dataset["train"].column_names
|
||||||
|
)
|
||||||
|
print("Dataset formatting completed.")
|
||||||
|
print("Example prompt:\n")
|
||||||
|
print(dataset["train"][0]["text"])
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [6/7] Training arguments
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[6/7] Initializing training arguments...")
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir=OUTPUT_DIR,
|
||||||
|
per_device_train_batch_size=1,
|
||||||
|
gradient_accumulation_steps=16,
|
||||||
|
learning_rate=1e-4,
|
||||||
|
num_train_epochs=2,
|
||||||
|
max_steps=1000,
|
||||||
|
|
||||||
|
fp16=False, # ⚠ disable AMP
|
||||||
|
bf16=False, # ⚠ disable BF16
|
||||||
|
|
||||||
|
optim="paged_adamw_32bit",
|
||||||
|
logging_steps=10,
|
||||||
|
save_steps=500,
|
||||||
|
save_total_limit=2,
|
||||||
|
report_to="none",
|
||||||
|
|
||||||
|
dataloader_pin_memory=False,
|
||||||
|
max_grad_norm=0.0, # avoid AMP gradient clipping
|
||||||
|
)
|
||||||
|
print("Training arguments ready.")
|
||||||
|
print(f"Output directory: {OUTPUT_DIR}")
|
||||||
|
print(f"Epochs: {training_args.num_train_epochs}")
|
||||||
|
print(f"Effective batch size: {training_args.per_device_train_batch_size * training_args.gradient_accumulation_steps}")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# [7/7] Trainer
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nInitializing SFTTrainer...")
|
||||||
|
trainer = SFTTrainer(
|
||||||
|
model=model,
|
||||||
|
train_dataset=dataset["train"],
|
||||||
|
args=training_args,
|
||||||
|
)
|
||||||
|
print("Trainer initialized.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Training
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nStarting training...")
|
||||||
|
checkpoint_exists = False
|
||||||
|
if os.path.exists(OUTPUT_DIR):
|
||||||
|
checkpoint_exists = any(
|
||||||
|
d.startswith("checkpoint-")
|
||||||
|
for d in os.listdir(OUTPUT_DIR)
|
||||||
|
)
|
||||||
|
|
||||||
|
if checkpoint_exists:
|
||||||
|
print("Checkpoint found → resuming training")
|
||||||
|
train_output = trainer.train(resume_from_checkpoint=True)
|
||||||
|
else:
|
||||||
|
print("No checkpoint found → starting fresh training")
|
||||||
|
train_output = trainer.train()
|
||||||
|
|
||||||
|
print("\n=== Training summary ===")
|
||||||
|
print(f"Global steps: {train_output.global_step}")
|
||||||
|
print(f"Training loss: {train_output.training_loss}")
|
||||||
|
print(f"Metrics: {train_output.metrics}")
|
||||||
|
print("Training completed successfully.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Save LoRA adapter and tokenizer
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nSaving LoRA adapter and tokenizer...")
|
||||||
|
trainer.model.save_pretrained(OUTPUT_DIR)
|
||||||
|
tokenizer.save_pretrained(OUTPUT_DIR)
|
||||||
|
print("\n=== Fine-tuning finished ===")
|
||||||
|
print(f"LoRA adapter saved in: {OUTPUT_DIR}")
|
||||||
68
Finetunning/mergeLora.py
Normal file
68
Finetunning/mergeLora.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import torch
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
|
from peft import PeftModel
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Configuration
|
||||||
|
# ----------------------------
|
||||||
|
BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct"
|
||||||
|
LORA_DIR = "./qwen2.5-7b-uk-fr-lora" # dossier issu du fine-tuning
|
||||||
|
OUTPUT_DIR = "./qwen2.5-7b-uk-fr-merged" # modèle fusionné final
|
||||||
|
|
||||||
|
DTYPE = torch.float16 # GGUF-friendly
|
||||||
|
DEVICE = "cpu" # merge sur CPU (stable, sûr)
|
||||||
|
|
||||||
|
print("=== LoRA merge script started ===")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load base model
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[1/4] Loading base model...")
|
||||||
|
base_model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
BASE_MODEL,
|
||||||
|
torch_dtype=DTYPE,
|
||||||
|
device_map=DEVICE,
|
||||||
|
trust_remote_code=True,
|
||||||
|
)
|
||||||
|
print("Base model loaded.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load tokenizer
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[2/4] Loading tokenizer...")
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
BASE_MODEL,
|
||||||
|
trust_remote_code=True
|
||||||
|
)
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
print("Tokenizer loaded.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load LoRA adapter
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[3/4] Loading LoRA adapter...")
|
||||||
|
model = PeftModel.from_pretrained(
|
||||||
|
base_model,
|
||||||
|
LORA_DIR,
|
||||||
|
)
|
||||||
|
print("LoRA adapter loaded.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Merge LoRA into base model
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\n[4/4] Merging LoRA into base model...")
|
||||||
|
model = model.merge_and_unload()
|
||||||
|
print("LoRA successfully merged.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Save merged model
|
||||||
|
# ----------------------------
|
||||||
|
print("Saving merged model...")
|
||||||
|
model.save_pretrained(
|
||||||
|
OUTPUT_DIR,
|
||||||
|
safe_serialization=True,
|
||||||
|
)
|
||||||
|
tokenizer.save_pretrained(OUTPUT_DIR)
|
||||||
|
|
||||||
|
print("=== Merge completed successfully ===")
|
||||||
|
print(f"Merged model saved in: {OUTPUT_DIR}")
|
||||||
33773
Finetunning/paires.json
Normal file
33773
Finetunning/paires.json
Normal file
File diff suppressed because it is too large
Load Diff
9349
Finetunning/paires_clean.json
Normal file
9349
Finetunning/paires_clean.json
Normal file
File diff suppressed because it is too large
Load Diff
35
Finetunning/tsv2json.py
Normal file
35
Finetunning/tsv2json.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Chemin vers ton fichier d'entrée et de sortie
|
||||||
|
input_file = "Paires-de-phrases-en-ukrainien-francais-2026-01-06.tsv" # Remplace par ton chemin
|
||||||
|
output_file = "paires.json" # Fichier de sortie
|
||||||
|
|
||||||
|
# Dictionnaire pour stocker les paires uniques (clé = phrase ukrainienne, valeur = liste de traductions)
|
||||||
|
unique_pairs = defaultdict(list)
|
||||||
|
|
||||||
|
# Lire le fichier d'entrée
|
||||||
|
with open(input_file, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
# Diviser la ligne en colonnes (séparateur = tabulation)
|
||||||
|
parts = line.strip().split("\t")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
uk_text = parts[1] # Texte ukrainien
|
||||||
|
fr_text = parts[3] # Traduction française
|
||||||
|
# Ajouter la paire au dictionnaire (évite les doublons)
|
||||||
|
if fr_text not in unique_pairs[uk_text]:
|
||||||
|
unique_pairs[uk_text].append(fr_text)
|
||||||
|
|
||||||
|
# Écrire le fichier JSONL de sortie
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f_out:
|
||||||
|
for uk_text, fr_translations in unique_pairs.items():
|
||||||
|
# Prendre la première traduction (ou toutes si tu préfères)
|
||||||
|
for fr_text in fr_translations:
|
||||||
|
# Créer une entrée JSONL
|
||||||
|
entry = {
|
||||||
|
"text": uk_text,
|
||||||
|
"translation": fr_text
|
||||||
|
}
|
||||||
|
f_out.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
print(f"Fichier JSONL généré : {output_file}")
|
||||||
30
Finetunning/validation.jsonl
Normal file
30
Finetunning/validation.jsonl
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{"text": "Як би ти не намагався, ти не вивчиш англійську за два-три місяці.", "translation": "Quels que soient tes efforts, tu ne pourras pas apprendre l’anglais en deux-trois mois."}
|
||||||
|
{"text": "Поки я не подзвонив, він не прийшов.", "translation": "Il n’est pas venu avant que je ne l’appelle."}
|
||||||
|
{"text": "У всесвіті багато галактик.", "translation": "Il y a beaucoup de galaxies dans l'univers."}
|
||||||
|
{"text": "Вона приймає душ щоранку.", "translation": "Elle prend une douche chaque matin."}
|
||||||
|
{"text": "У Майка є декілька друзів у Флориді.", "translation": "Mike a quelques amis en Floride."}
|
||||||
|
{"text": "Я зустрінуся з тобою в неділю о третій.", "translation": "On se voit dimanche à trois heures."}
|
||||||
|
{"text": "Я сказав собі: «Це гарна ідея».", "translation": "Je me suis dit : « C’est une bonne idée. »"}
|
||||||
|
{"text": "Ми збиралися пробути там біля двох тижнів.", "translation": "Nous avions l’intention de rester là près de deux semaines."}
|
||||||
|
{"text": "Я чищу зуби двічі на день.", "translation": "Je me brosse les dents deux fois par jour."}
|
||||||
|
{"text": "Він ніжно поклав руку на її плече.", "translation": "Il posa la main gentiment sur son épaule."}
|
||||||
|
{"text": "Сьогодні жахливо холодно.", "translation": "Il fait horriblement froid aujourd'hui."}
|
||||||
|
{"text": "У цю суму включено податки.", "translation": "Cette somme inclut les taxes."}
|
||||||
|
{"text": "Ця школа була заснована в 1650 році.", "translation": "Cette école fut fondée en 1650."}
|
||||||
|
{"text": "Я випадково знайшов цей ресторан.", "translation": "J'ai trouvé ce restaurant par hasard."}
|
||||||
|
{"text": "Я не хотів нікого образити.", "translation": "Je ne voulais vexer personne."}
|
||||||
|
{"text": "Цей сад найкраще виглядає весною.", "translation": "Ce parc est plus joli au printemps."}
|
||||||
|
{"text": "Цей сир виготовлено з овечого молока.", "translation": "Ce fromage est fait avec du lait de chèvre."}
|
||||||
|
{"text": "Він спить як немовля.", "translation": "Il dort comme un bébé."}
|
||||||
|
{"text": "Гора вкрита снігом.", "translation": "La montagne est recouverte de neige."}
|
||||||
|
{"text": "Я попав під дощ і промок.", "translation": "J’ai été pris sous la pluie, et suis tout trempé."}
|
||||||
|
{"text": "Прошу, дайте мені ще один шанс.", "translation": "Je vous en prie, donnez-moi encore une chance."}
|
||||||
|
{"text": "Я все сказав.", "translation": "J’ai tout dit."}
|
||||||
|
{"text": "Не забувай нас!", "translation": "Ne nous oublie pas !"}
|
||||||
|
{"text": "Випало багато снігу.", "translation": "Beaucoup de neige est tombée."}
|
||||||
|
{"text": "Йде сніг.", "translation": "Il est en train de neiger."}
|
||||||
|
{"text": "Може піти сніг.", "translation": "Il neigera peut-être."}
|
||||||
|
{"text": "У нас у січні йде сніг.", "translation": "Chez nous, il neige en janvier."}
|
||||||
|
{"text": "Сніг розтав.", "translation": "La neige a fondu."}
|
||||||
|
{"text": "Наша компанія планує побудувати новий хімічний завод у Росії.", "translation": "Notre entreprise a le projet de construire une nouvelle usine chimique en Russie."}
|
||||||
|
{"text": "Франція воювала з Росією.", "translation": "La France fut en guerre avec la Russie."}
|
||||||
170
Finetunning/validation.py
Normal file
170
Finetunning/validation.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import torch
|
||||||
|
from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig
|
||||||
|
from peft import PeftModel
|
||||||
|
from datasets import load_dataset
|
||||||
|
from nltk.translate.bleu_score import corpus_bleu
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Configuration
|
||||||
|
# ----------------------------
|
||||||
|
BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct" # base model
|
||||||
|
LORA_DIR = "./qwen2.5-7b-uk-fr-lora-2epoch" # fine-tuned LoRA
|
||||||
|
VALIDATION_FILE = "validation.jsonl" # small validation subset
|
||||||
|
MAX_INPUT_LENGTH = 1024
|
||||||
|
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
|
# Liste des prompts à tester
|
||||||
|
PROMPTS_TO_TEST = [
|
||||||
|
{
|
||||||
|
"name": "Prompt de base",
|
||||||
|
"prompt": "Traduis la phrase ukrainienne suivante en français: {text}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prompt spécialisé mémoires",
|
||||||
|
"prompt": (
|
||||||
|
"Tu es un traducteur spécialisé dans les mémoires ukrainiennes des années 1910.\n"
|
||||||
|
"- Garde le style narratif et les tournures orales de l'auteur.\n"
|
||||||
|
"- Respecte les règles de traduction suivantes :\n\n"
|
||||||
|
"Règles strictes :\n"
|
||||||
|
"1. **Conserve tous les noms de lieux** dans leur forme originale (ex. : Львів → Lviv, mais ajoute une note si nécessaire entre [ ]).\n"
|
||||||
|
"2. **Respecte le style narratif** : garde les tournures orales et les expressions propres à l'auteur.\n\n"
|
||||||
|
"Voici la phrase à traduire :\nUkrainien : {text}\nFrançais :"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prompt détaillé",
|
||||||
|
"prompt": (
|
||||||
|
"Tu es un expert en traduction littéraire spécialisé dans les textes historiques ukrainiens.\n"
|
||||||
|
"Règles à suivre absolument :\n"
|
||||||
|
"1. Conserve tous les noms propres et toponymes dans leur forme originale\n"
|
||||||
|
"2. Préserve le style et le registre de l'auteur original\n"
|
||||||
|
"3. Ajoute des notes entre crochets pour expliquer les références culturelles si nécessaire\n"
|
||||||
|
"4. Traduis de manière naturelle en français tout en restant fidèle au texte source\n\n"
|
||||||
|
"Texte à traduire :\nUkrainien : {text}\nTraduction française :"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prompt minimaliste",
|
||||||
|
"prompt": "Traduction fidèle de l'ukrainien vers le français : {text}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
print("=== Loading tokenizer and model ===")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load tokenizer
|
||||||
|
# ----------------------------
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
BASE_MODEL,
|
||||||
|
trust_remote_code=True
|
||||||
|
)
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
tokenizer.model_max_length = MAX_INPUT_LENGTH
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load base model directly on GPU
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nLoading base model on GPU...")
|
||||||
|
base_model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
BASE_MODEL,
|
||||||
|
torch_dtype=torch.float16,
|
||||||
|
device_map={"": 0}, # all on GPU
|
||||||
|
trust_remote_code=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Apply LoRA adapter
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nApplying LoRA adapter...")
|
||||||
|
model = PeftModel.from_pretrained(base_model, LORA_DIR)
|
||||||
|
model.eval()
|
||||||
|
model.to(DEVICE) # ensure everything on GPU
|
||||||
|
print("Model ready for validation.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Load validation dataset
|
||||||
|
# ----------------------------
|
||||||
|
print(f"{80 * '_'}\nLoading validation dataset...")
|
||||||
|
dataset = load_dataset("json", data_files=VALIDATION_FILE)
|
||||||
|
examples = dataset["train"]
|
||||||
|
print(f"{len(examples)} examples loaded for testing.")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Translation function
|
||||||
|
# ----------------------------
|
||||||
|
@torch.inference_mode()
|
||||||
|
def translate(text, prompt_template):
|
||||||
|
prompt = prompt_template.format(text=text)
|
||||||
|
inputs = tokenizer(
|
||||||
|
prompt,
|
||||||
|
return_tensors="pt",
|
||||||
|
truncation=True,
|
||||||
|
max_length=MAX_INPUT_LENGTH
|
||||||
|
).to(DEVICE)
|
||||||
|
|
||||||
|
# Utilisation de GenerationConfig pour éviter les avertissements
|
||||||
|
generation_config = GenerationConfig.from_model_config(model.config)
|
||||||
|
generation_config.max_new_tokens = 256
|
||||||
|
generation_config.do_sample = False
|
||||||
|
|
||||||
|
outputs = model.generate(
|
||||||
|
**inputs,
|
||||||
|
generation_config=generation_config
|
||||||
|
)
|
||||||
|
|
||||||
|
result = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
||||||
|
|
||||||
|
# Extraction de la partie traduction
|
||||||
|
if "Français :" in result:
|
||||||
|
translation_part = result.split("Français :")[-1].strip()
|
||||||
|
elif "Traduction française :" in result:
|
||||||
|
translation_part = result.split("Traduction française :")[-1].strip()
|
||||||
|
else:
|
||||||
|
translation_part = result.split(text)[-1].strip()
|
||||||
|
|
||||||
|
return translation_part
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Evaluate all prompts and select best BLEU
|
||||||
|
# ----------------------------
|
||||||
|
best_bleu = 0
|
||||||
|
best_prompt = None
|
||||||
|
all_results = {}
|
||||||
|
|
||||||
|
print(f"{80 * '_'}\nTesting all prompts and computing BLEU scores...")
|
||||||
|
|
||||||
|
for prompt_config in PROMPTS_TO_TEST:
|
||||||
|
print(f"\n{80 * '='}\nTesting prompt: {prompt_config['name']}\n{80 * '='}")
|
||||||
|
references = []
|
||||||
|
hypotheses = []
|
||||||
|
|
||||||
|
for i, example in enumerate(examples):
|
||||||
|
src_text = example["text"]
|
||||||
|
ref_text = example["translation"]
|
||||||
|
pred_text = translate(src_text, prompt_config["prompt"])
|
||||||
|
|
||||||
|
print(f"\n[{i+1}] Source: {src_text}")
|
||||||
|
print(f" Reference: {ref_text}")
|
||||||
|
print(f" Prediction: {pred_text}")
|
||||||
|
|
||||||
|
references.append([ref_text.split()])
|
||||||
|
hypotheses.append(pred_text.split())
|
||||||
|
|
||||||
|
bleu_score = corpus_bleu(references, hypotheses) * 100
|
||||||
|
print(f"\n=== Corpus BLEU score for '{prompt_config['name']}': {bleu_score:.4f} ===")
|
||||||
|
|
||||||
|
all_results[prompt_config["name"]] = bleu_score
|
||||||
|
|
||||||
|
if bleu_score > best_bleu:
|
||||||
|
best_bleu = bleu_score
|
||||||
|
best_prompt = prompt_config
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Display results
|
||||||
|
# ----------------------------
|
||||||
|
print(f"\n{80 * '='}\nFINAL RESULTS\n{80 * '='}")
|
||||||
|
for prompt_name, score in all_results.items():
|
||||||
|
print(f"{prompt_name}: {score:.4f}")
|
||||||
|
|
||||||
|
print(f"\nBEST PROMPT: {best_prompt['name']} with BLEU score: {best_bleu:.4f}")
|
||||||
|
print(f"Prompt content:\n{best_prompt['prompt']}")
|
||||||
14
Modelfile
14
Modelfile
@@ -1,14 +0,0 @@
|
|||||||
FROM zongwei/gemma3-translator:4b
|
|
||||||
PARAMETER temperature 0.9
|
|
||||||
PARAMETER num_ctx 131072
|
|
||||||
SYSTEM """
|
|
||||||
Tu es un traducteur professionnel spécialisé dans la traduction de texte ukrainien en français.
|
|
||||||
Tu est spécialisé dans la rédaction des textes auto-biographiques et historiques et parles un français impécable.
|
|
||||||
Tu dois toujours répondre en français et uniquement dans cette langue.
|
|
||||||
Tu prends soins des congugaisons et de la tournure de phases.
|
|
||||||
N'ajoutes aucun texte sous quelle forme que ce soit avant ou après le texte traduit. Ne réponds qu'avec le texte traduit.
|
|
||||||
N'inclus jamais la phrase "Voici le texte traduit" dans la réponse.
|
|
||||||
Traduis avec précision et naturel, en respectant l'intonation originale utilisée par l'auteur du texte.
|
|
||||||
Tu ne dois pas interpréter les pensées ou les réflexions de l'auteur, la traduciton doit restée fidèle à la pensée de l'auteur.
|
|
||||||
Le texte que tu traduis est un texte historique, tu ne dois pas le changer.
|
|
||||||
"""
|
|
||||||
149
README.md
149
README.md
@@ -9,29 +9,6 @@ Ce projet permet de traduire un document PDF page par page en utilisant un modè
|
|||||||
- **Python** (version 3.8 ou supérieure)
|
- **Python** (version 3.8 ou supérieure)
|
||||||
- **Ollama** installé et en cours d'exécution sur ta machine (en mode "serveur")
|
- **Ollama** installé et en cours d'exécution sur ta machine (en mode "serveur")
|
||||||
- Un **document PDF** à traduire
|
- Un **document PDF** à traduire
|
||||||
- Un modèle LLM spécialisé dans la traduction avec un context long.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Création d'un modèle LLM de traduction avec Ollama
|
|
||||||
En partant du LLM [zongwei/gemma3-translator:4b](https://ollama.com/zongwei/gemma3-translator), nous allons créer un modèle optimisé pour la traduction avec Ollama.
|
|
||||||
Pour info : Inutile de le downloader, il le serra automatiquement au lancement de la commande.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
FROM zongwei/gemma3-translator:4b
|
|
||||||
PARAMETER temperature 0.1
|
|
||||||
PARAMETER num_ctx 131072
|
|
||||||
SYSTEM """
|
|
||||||
You are a professional translator specialising in translating Ukrainian text into English.
|
|
||||||
Translate accurately and naturally, respecting the original intonation used by the author of the text.
|
|
||||||
You must not interpret the author's thoughts or reflections.
|
|
||||||
Do not add any text before or after the text provided.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
Il faut ensuite compiler le modèle avec la commande :
|
|
||||||
```
|
|
||||||
ollama create traductionUkrainienVersFrancais -f .\Modelfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -44,11 +21,16 @@ ollama create traductionUkrainienVersFrancais -f .\Modelfile
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Installer les dépendances Python**
|
2. **Installer les dépendances Python**
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Placer votre fichier PDF** dans le répertoire du projet avec le nom configuré dans `main.py` (par défaut : `TaniaBorecMemoir(Ukr).pdf`)
|
Puis faire :
|
||||||
|
```bash
|
||||||
|
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Placer votre fichier PDF** dans le répertoire `Traduction` du projet avec le nom configuré dans `main.py` (par défaut : `TaniaBorecMemoir(Ukr).pdf`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,37 +39,16 @@ ollama create traductionUkrainienVersFrancais -f .\Modelfile
|
|||||||
### Préparation
|
### Préparation
|
||||||
|
|
||||||
1. **Démarrer Ollama en mode serveur** sur votre machine (port 11434 par défaut)
|
1. **Démarrer Ollama en mode serveur** sur votre machine (port 11434 par défaut)
|
||||||
```bash
|
```bash
|
||||||
ollama serve
|
ollama serve
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Vérifier que le modèle de traduction est disponible**
|
|
||||||
```bash
|
|
||||||
ollama list
|
|
||||||
```
|
|
||||||
Vous devez voir `traductionUkrainienVersFrancais` dans la liste.
|
|
||||||
|
|
||||||
Si ce n'est pas le cas, vous devez le générer comme décrit plus haut (paragraphe "Création d'un modèle LLM de traduction avec Ollama")
|
|
||||||
|
|
||||||
3. **Placer votre PDF** dans le même répertoire que le script `main.py`
|
|
||||||
### Paramétrage du script
|
|
||||||
`PDF_PATH`= "TaniaBorecMemoir(Ukr).pdf" <- Le nom du fichier pdf à traduire.
|
|
||||||
`OLLAMA_MODEL` = "mitmul/plamo-2-translate:latest" <- Le nom
|
|
||||||
`OLLAMA_URL` = "http://localhost:11434/api/generate" <- URL par défaut d'Ollama
|
|
||||||
`TARGET_LANGUAGE` = "français" <- Langue cible (ex: "français", "anglais", "allemand", "espagnol", etc.)
|
|
||||||
`SYSTEM_PROMPT` = """You are a professional translator specialising in Ukrainian text translation.
|
|
||||||
Translate accurately and naturally, respecting the original intonation used by the author of the text.
|
|
||||||
You must not interpret the author's thoughts or reflections.
|
|
||||||
Do not add any text before or after the text provided.
|
|
||||||
Preserve the layout and structure of the original text."""
|
|
||||||
|
|
||||||
|
|
||||||
### Exécution
|
### Exécution
|
||||||
|
|
||||||
1. **Lancer le script de traduction**
|
1. **Lancer le script de traduction**
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Le programme va :**
|
2. **Le programme va :**
|
||||||
- Extraire le texte de toutes les pages du PDF
|
- Extraire le texte de toutes les pages du PDF
|
||||||
@@ -97,8 +58,8 @@ Preserve the layout and structure of the original text."""
|
|||||||
- Envoyer chaque chunk au LLM pour traduction
|
- Envoyer chaque chunk au LLM pour traduction
|
||||||
- Afficher la progression dans le terminal
|
- Afficher la progression dans le terminal
|
||||||
|
|
||||||
3. **Le résultat final** sera généré dans un fichier PDF nommé `[nom_original] (FR).pdf`
|
3. **Le résultat final** sera généré dans un fichier PDF nommé `[nom_original](FR).pdf`
|
||||||
- Exemple : `TaniaBorecMemoir(Ukr) (FR).pdf`
|
- Exemple : `TaniaBorecMemoir(Ukr)(FR).pdf`
|
||||||
|
|
||||||
### Fichier de sortie
|
### Fichier de sortie
|
||||||
|
|
||||||
@@ -120,18 +81,74 @@ Vous pouvez modifier les paramètres suivants dans `main.py` :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dépannage
|
## Finnetunning
|
||||||
|
/!\ Expérimental !!!
|
||||||
|
|
||||||
### Erreur : "Connexion refusée à Ollama"
|
Le finne-tunning permet d'avoir une meilleur traduction. C'est un processus long en temps de calcul, mais permet une traduction plus précise.
|
||||||
- Vérifiez que Ollama est en cours d'exécution avec `ollama serve`
|
|
||||||
- Vérifiez que l'URL est correcte (par défaut : `http://localhost:11434`)
|
|
||||||
|
|
||||||
### Erreur : "Modèle non trouvé"
|
Le principe est le suivant :
|
||||||
- Exécutez : `ollama create traductionUkrainienVersFrancais -f .\Modelfile`
|
|
||||||
|
|
||||||
### Le texte n'est pas bien séparé en paragraphes
|
```
|
||||||
- Le script détecte automatiquement les doubles sauts de ligne
|
1️⃣ Dataset d’entraînement (pairs.json)
|
||||||
- Si absent, il divise par phrases et regroupe en chunks de 1500 caractères
|
↓
|
||||||
|
1️⃣ Dataset nettoyé ( cleanDataSet.py -> pairs_clean.json)
|
||||||
|
↓
|
||||||
|
2️⃣ Fine-tuning LoRA (finetuning.py)
|
||||||
|
↓
|
||||||
|
3️⃣ Validation / Évaluation BLEU (validation.py)
|
||||||
|
↓
|
||||||
|
4️⃣ Merge LoRA + modèle de base (mergeLora.py)
|
||||||
|
↓
|
||||||
|
5️⃣ Conversion en GGUF ()
|
||||||
|
↓
|
||||||
|
6️⃣ Ollama (inférence finale)
|
||||||
|
|
||||||
---
|
```
|
||||||
|
|
||||||
|
### Nettoyage du dataset
|
||||||
|
Executer le script ```python cleanDataSet.py```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
Executer le script ```python validation.py```
|
||||||
|
|
||||||
|
Le script tests plusieurs prompts et renvoie celui avec le meilleur score BLEU.
|
||||||
|
|
||||||
|
Il faut ensuite copier ce prompt dans le fichier ModelFile.
|
||||||
|
|
||||||
|
### Merge
|
||||||
|
Executer le script ```python mergeLora.py```
|
||||||
|
|
||||||
|
### Conversion en GGUF
|
||||||
|
En étant à la racine du projet (et toujours dans le venv), cloner le projet llama.cpp
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/ggerganov/llama.cpp
|
||||||
|
cd llama.cpp
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Et lancer la commande (/!\ ca prend eviron 10 minutes):
|
||||||
|
```bash
|
||||||
|
python convert_hf_to_gguf.py ../Finetunning/qwen2.5-7b-uk-fr-merged --outfile qwen2.5-7b-uk-fr.gguf --outtype q8_0
|
||||||
|
```
|
||||||
|
|
||||||
|
Vérification :
|
||||||
|
```bash
|
||||||
|
./main -m qwen2.5-7b-uk-fr.gguf -p "Translate into French: Привіт світ"
|
||||||
|
```
|
||||||
|
Pour que ce nouveau modèle soit exploitable par ollama, il faut TODO
|
||||||
|
|
||||||
|
## Utilisation du modèle fine-tunné pour la traduction
|
||||||
|
Créer un Modelfile :
|
||||||
|
```
|
||||||
|
FROM ./qwen2.5-7b-uk-fr.gguf
|
||||||
|
|
||||||
|
PARAMETER temperature 0.1
|
||||||
|
PARAMETER top_p 0.95
|
||||||
|
PARAMETER num_ctx 4096
|
||||||
|
|
||||||
|
SYSTEM """
|
||||||
|
You are a professional Ukrainian to French translator.
|
||||||
|
Produce faithful, literal translations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
```
|
||||||
Binary file not shown.
19
Traduction/Modelfile
Normal file
19
Traduction/Modelfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM translategemma:12b
|
||||||
|
PARAMETER temperature 0.2
|
||||||
|
PARAMETER num_ctx 8192
|
||||||
|
|
||||||
|
SYSTEM """
|
||||||
|
You are a professional Ukrainian (uk) to french (fr) translator. Your goal is to accurately convey the meaning and nuances of the original Ukrainian text while adhering to french grammar, vocabulary, and cultural sensitivities.
|
||||||
|
Produce only the french translation, without any additional explanations or commentary. Please translate the following Ukrainian text into french:
|
||||||
|
|
||||||
|
|
||||||
|
{TEXT}
|
||||||
|
|
||||||
|
Règles strictes :
|
||||||
|
1. **Conserve tous les noms de lieux** dans leur forme originale (ex. : Львів → Lviv, mais ajoute une note si nécessaire entre [ ]).
|
||||||
|
2. **Respecte le style narratif** : garde les tournures orales et les expressions propres à l’auteur.
|
||||||
|
3. **Pour les termes historiques** (ex. : "powiat"), utilise le terme français standard et ajoute une note explicative.
|
||||||
|
4. **Conserve les citations** russe/allemand/polonais intégrés au texte (mais ajoute une note de fin de paragraphe entre [ ] en la traduisant et en précisant la langue d'origine.
|
||||||
|
5. **Structure** : Garde les sauts de ligne et la mise en page originale.
|
||||||
|
6. **Notes du traducteur** : Ajoute entre crochets [ ] les explications contextuelles si un contexte historique existe (ex. : "[Note : le context]").
|
||||||
|
"""
|
||||||
@@ -8,18 +8,20 @@ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|||||||
from reportlab.lib.enums import TA_JUSTIFY
|
from reportlab.lib.enums import TA_JUSTIFY
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
import os
|
import os, time
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
PDF_PATH = "TaniaBorecMemoir(Ukr).pdf"
|
PDF_PATH = "Traduction/TaniaBorecMemoir(Ukr).pdf"
|
||||||
OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest"
|
OLLAMA_MODEL = "traductionUkrainienVersFrancais:latest"
|
||||||
OLLAMA_URL = "http://localhost:11434/api/generate"
|
OLLAMA_URL = "http://localhost:11434/api/generate"
|
||||||
TARGET_LANGUAGE = "français"
|
TARGET_LANGUAGE = "français"
|
||||||
CHECKPOINT_FILE = "checkpoint.json"
|
CHECKPOINT_FILE = "Traduction/checkpoint.json"
|
||||||
TEMP_OUTPUT_TXT = "output_temp.txt"
|
TEMP_OUTPUT_TXT = "Traduction/output_temp.txt"
|
||||||
FINAL_OUTPUT_PDF = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.pdf")
|
FINAL_OUTPUT_PDF = PDF_PATH.replace(".pdf",f"({TARGET_LANGUAGE.upper()[:2]})_V10.pdf")
|
||||||
FINAL_OUTPUT_TXT = PDF_PATH.replace(".pdf",f" ({TARGET_LANGUAGE.upper()[:2]})_V2.txt")
|
FINAL_OUTPUT_TXT = PDF_PATH.replace(".pdf",f"({TARGET_LANGUAGE.upper()[:2]})_V10.txt")
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
def extract_parameters_from_template(template_str):
|
def extract_parameters_from_template(template_str):
|
||||||
"""Extrait les paramètres du modèle à partir du template."""
|
"""Extrait les paramètres du modèle à partir du template."""
|
||||||
@@ -38,7 +40,7 @@ def extract_parameters_from_template(template_str):
|
|||||||
params_section = template_str
|
params_section = template_str
|
||||||
|
|
||||||
# Parse les lignes de paramètres
|
# Parse les lignes de paramètres
|
||||||
# Format: "stop "<end_of_turn>""
|
# Format: "stop "<end_of_turn>""
|
||||||
# "temperature 0.1"
|
# "temperature 0.1"
|
||||||
lines = params_section.split('\n')
|
lines = params_section.split('\n')
|
||||||
|
|
||||||
@@ -144,8 +146,8 @@ def display_llm_info():
|
|||||||
return "Informations du modèle non disponibles"
|
return "Informations du modèle non disponibles"
|
||||||
|
|
||||||
def register_unicode_font():
|
def register_unicode_font():
|
||||||
"""Enregistre une police TrueType qui supporte le cyrilique."""
|
"""Enregistre une police TrueType qui supporte le cyrillique."""
|
||||||
# Recherche une police système qui supporte le cyrilique
|
# Recherche une police système qui supporte le cyrillique
|
||||||
font_paths = [
|
font_paths = [
|
||||||
r"C:\Windows\Fonts\DejaVuSans.ttf",
|
r"C:\Windows\Fonts\DejaVuSans.ttf",
|
||||||
r"C:\Windows\Fonts\Calibri.ttf",
|
r"C:\Windows\Fonts\Calibri.ttf",
|
||||||
@@ -173,8 +175,12 @@ def load_checkpoint():
|
|||||||
|
|
||||||
# Sauvegarde le checkpoint
|
# Sauvegarde le checkpoint
|
||||||
def save_checkpoint(last_index, results):
|
def save_checkpoint(last_index, results):
|
||||||
|
# Trier les clés du dictionnaire results
|
||||||
|
sorted_results = {key: results[key] for key in sorted(results.keys(), key=int)}
|
||||||
|
|
||||||
with open(CHECKPOINT_FILE, "w") as f:
|
with open(CHECKPOINT_FILE, "w") as f:
|
||||||
json.dump({"last_processed_index": last_index, "results": results}, f)
|
# Utiliser un espace d'indentation de 4 espaces pour rendre le JSON plus lisible
|
||||||
|
json.dump({"last_processed_index": last_index, "results": sorted_results}, f, indent=4)
|
||||||
|
|
||||||
# Sauvegarde les résultats temporaires dans un fichier TXT
|
# Sauvegarde les résultats temporaires dans un fichier TXT
|
||||||
def save_temp_results(results):
|
def save_temp_results(results):
|
||||||
@@ -211,16 +217,13 @@ def send_to_ollama(text, target_lang=TARGET_LANGUAGE, model=OLLAMA_MODEL):
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"Erreur Ollama: {response.text}")
|
raise Exception(f"Erreur Ollama: {response.text}")
|
||||||
|
|
||||||
# Création du PDF final (inchangée)
|
# Création du PDF final avec numéros de chapitres dans la marge
|
||||||
def create_pdf_from_results(results, output_path):
|
def create_pdf_from_results(results, output_path):
|
||||||
"""Crée un PDF à partir des résultats de traduction."""
|
"""Crée un PDF à partir des résultats de traduction, avec des notes dans la marge et un numéro de page."""
|
||||||
doc = SimpleDocTemplate(output_path, pagesize=letter, topMargin=inch, bottomMargin=inch)
|
|
||||||
story = []
|
story = []
|
||||||
|
|
||||||
# Enregistre une police qui supporte le cyrilique
|
|
||||||
font_name = register_unicode_font()
|
font_name = register_unicode_font()
|
||||||
|
|
||||||
# Style personnalisé
|
# Styles personnalisés
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
title_style = ParagraphStyle(
|
title_style = ParagraphStyle(
|
||||||
'CustomTitle',
|
'CustomTitle',
|
||||||
@@ -231,7 +234,7 @@ def create_pdf_from_results(results, output_path):
|
|||||||
alignment=TA_JUSTIFY,
|
alignment=TA_JUSTIFY,
|
||||||
fontName=font_name
|
fontName=font_name
|
||||||
)
|
)
|
||||||
|
|
||||||
page_style = ParagraphStyle(
|
page_style = ParagraphStyle(
|
||||||
'PageHeading',
|
'PageHeading',
|
||||||
parent=styles['Heading2'],
|
parent=styles['Heading2'],
|
||||||
@@ -241,7 +244,7 @@ def create_pdf_from_results(results, output_path):
|
|||||||
spaceBefore=0.2*inch,
|
spaceBefore=0.2*inch,
|
||||||
fontName=font_name
|
fontName=font_name
|
||||||
)
|
)
|
||||||
|
|
||||||
body_style = ParagraphStyle(
|
body_style = ParagraphStyle(
|
||||||
'CustomBody',
|
'CustomBody',
|
||||||
parent=styles['BodyText'],
|
parent=styles['BodyText'],
|
||||||
@@ -250,27 +253,45 @@ def create_pdf_from_results(results, output_path):
|
|||||||
spaceAfter=0.2*inch,
|
spaceAfter=0.2*inch,
|
||||||
fontName=font_name
|
fontName=font_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
note_style = ParagraphStyle(
|
||||||
|
'CustomBody',
|
||||||
|
parent=styles['BodyText'],
|
||||||
|
fontSize=8,
|
||||||
|
alignment=TA_JUSTIFY,
|
||||||
|
spaceAfter=0,
|
||||||
|
fontName=font_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Création du document avec les callbacks pour les notes et le numéro de page
|
||||||
|
doc = SimpleDocTemplate(
|
||||||
|
output_path,
|
||||||
|
pagesize=letter,
|
||||||
|
topMargin=inch,
|
||||||
|
bottomMargin=inch,
|
||||||
|
)
|
||||||
# Titre avec la langue cible
|
# Titre avec la langue cible
|
||||||
story.append(Paragraph(f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}", title_style))
|
story.append(Paragraph(f"Traduction - Ukrainien vers {TARGET_LANGUAGE.capitalize()}", title_style))
|
||||||
story.append(Paragraph(f"Document : {PDF_PATH}", title_style))
|
story.append(Paragraph(f"Document : {PDF_PATH}", title_style))
|
||||||
story.append(Spacer(1, 0.2*inch))
|
story.append(Spacer(1, 0.2*inch))
|
||||||
|
|
||||||
# Contenu
|
# Contenu
|
||||||
for page_num, translation in results.items():
|
for paragraph_num, translation in results.items():
|
||||||
# Préserver la mise en page en convertissant les sauts de ligne
|
|
||||||
formatted_text = translation.replace("\n", "<br/>")
|
formatted_text = translation.replace("\n", "<br/>")
|
||||||
|
if DEBUG:
|
||||||
|
# Ajoute le paragraphe avec sa note
|
||||||
|
story.append(Paragraph(paragraph_num, note_style))
|
||||||
story.append(Paragraph(formatted_text, body_style))
|
story.append(Paragraph(formatted_text, body_style))
|
||||||
# story.append(Spacer(1, 0.1*inch))
|
|
||||||
|
|
||||||
# Infos sur le LLM
|
# Infos sur le LLM
|
||||||
story.append(Spacer(1, 0.2*inch))
|
story.append(Spacer(1, 0.2*inch))
|
||||||
story.append(Paragraph(display_llm_info(), page_style))
|
story.append(Paragraph(display_llm_info(), page_style))
|
||||||
|
|
||||||
# Construction du PDF
|
# Construction du PDF
|
||||||
doc.build(story)
|
doc.build(story)
|
||||||
print(f"PDF généré avec succès : {output_path}")
|
print(f"PDF généré avec succès : {output_path}")
|
||||||
|
|
||||||
|
|
||||||
def create_txt_from_results(results, output_path):
|
def create_txt_from_results(results, output_path):
|
||||||
"""Crée un fichier TXT à partir des résultats de traduction."""
|
"""Crée un fichier TXT à partir des résultats de traduction."""
|
||||||
OUTPUT_TXT_PATH = output_path.replace(".pdf", f".txt") # Chemin du fichier TXT de sortie
|
OUTPUT_TXT_PATH = output_path.replace(".pdf", f".txt") # Chemin du fichier TXT de sortie
|
||||||
@@ -281,7 +302,10 @@ def create_txt_from_results(results, output_path):
|
|||||||
txt_file.write(title_text + "\n\n")
|
txt_file.write(title_text + "\n\n")
|
||||||
|
|
||||||
# Contenu
|
# Contenu
|
||||||
for page_num, translation in results.items():
|
for paragraph_num, translation in results.items():
|
||||||
|
# Ajoute les numéro de paragraphe et chapitre
|
||||||
|
if(DEBUG): txt_file.write(f"{paragraph_num}\n")
|
||||||
|
|
||||||
# Préserver la mise en page en convertissant les sauts de ligne
|
# Préserver la mise en page en convertissant les sauts de ligne
|
||||||
txt_file.write(translation + "\n\n")
|
txt_file.write(translation + "\n\n")
|
||||||
|
|
||||||
@@ -291,17 +315,58 @@ def create_txt_from_results(results, output_path):
|
|||||||
|
|
||||||
# Fonction principale
|
# Fonction principale
|
||||||
def main():
|
def main():
|
||||||
# Charge le checkpoint
|
|
||||||
checkpoint = load_checkpoint()
|
checkpoint = load_checkpoint()
|
||||||
last_index = checkpoint["last_processed_index"]
|
last_index = checkpoint["last_processed_index"]
|
||||||
results = checkpoint["results"]
|
results = checkpoint["results"]
|
||||||
|
|
||||||
# Extraction des paragraphes
|
|
||||||
pages = extract_text_from_pdf(PDF_PATH)
|
pages = extract_text_from_pdf(PDF_PATH)
|
||||||
paragraphs = split_pages_in_paragraphs(pages)
|
paragraphs = split_pages_in_paragraphs(pages)
|
||||||
|
|
||||||
# Traitement des paragraphes
|
# Liste de tous les indices de batches attendus (par pas de batch_size)
|
||||||
batch_size = 3
|
batch_size = 5
|
||||||
|
expected_batch_indices = list(range(0, len(paragraphs), batch_size))
|
||||||
|
|
||||||
|
# Liste des indices de batches déjà présents dans results
|
||||||
|
present_batch_indices = set()
|
||||||
|
for key in results.keys():
|
||||||
|
batch_start = int(int(key) // batch_size * batch_size) # Arrondit à l'indice de début de batch
|
||||||
|
present_batch_indices.add(batch_start)
|
||||||
|
|
||||||
|
# Trouve les batches manquants
|
||||||
|
missing_batches = [i for i in expected_batch_indices if i not in present_batch_indices and i <= last_index]
|
||||||
|
|
||||||
|
# Affichage des batches manquants (pour débogage)
|
||||||
|
print(f"Batches manquants détectés : {missing_batches}")
|
||||||
|
|
||||||
|
# Traduction des paragraphes manquants
|
||||||
|
temps_cumule = 0.0
|
||||||
|
for i in missing_batches:
|
||||||
|
batch = paragraphs[i:i + batch_size]
|
||||||
|
paragraph_cumul = "\n".join(batch)
|
||||||
|
|
||||||
|
print(f"{15 * '-'} Traduction des paragraphes manquants {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
debut_chrono = time.time()
|
||||||
|
result = send_to_ollama(paragraph_cumul)
|
||||||
|
fin_chrono = time.time()
|
||||||
|
temps_paragraphe = fin_chrono - debut_chrono
|
||||||
|
temps_cumule += temps_paragraphe
|
||||||
|
|
||||||
|
# Conversion en minutes et secondes
|
||||||
|
minutes_paragraphe, secondes_paragraphe = divmod(temps_paragraphe, 60)
|
||||||
|
minutes_cumule, secondes_cumule = divmod(temps_cumule, 60)
|
||||||
|
|
||||||
|
print(f"{result}")
|
||||||
|
results[str(i)] = result
|
||||||
|
save_checkpoint(len(paragraphs), results) # Met à jour le dernier indice du batch
|
||||||
|
save_temp_results(results)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de la traduction du paragraphe {i}: {e}")
|
||||||
|
print(f" Temps de traduction : {int(minutes_paragraphe)} min {secondes_paragraphe:.2f} sec")
|
||||||
|
print(f" Temps cumulé : {int(minutes_cumule)} min {secondes_cumule:.2f} sec")
|
||||||
|
|
||||||
|
# Traitement des paragraphes suivants
|
||||||
for i in range(last_index + 1, len(paragraphs), batch_size):
|
for i in range(last_index + 1, len(paragraphs), batch_size):
|
||||||
batch = paragraphs[i:i + batch_size]
|
batch = paragraphs[i:i + batch_size]
|
||||||
paragraph_cumul = "\n".join(batch)
|
paragraph_cumul = "\n".join(batch)
|
||||||
@@ -309,20 +374,32 @@ def main():
|
|||||||
print(f"{15 * '-'} Traduction des paragraphes {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}")
|
print(f"{15 * '-'} Traduction des paragraphes {i+1} à {min(i + batch_size, len(paragraphs))} / {len(paragraphs)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
debut_chrono = time.time()
|
||||||
result = send_to_ollama(paragraph_cumul)
|
result = send_to_ollama(paragraph_cumul)
|
||||||
|
fin_chrono = time.time()
|
||||||
|
temps_paragraphe = fin_chrono - debut_chrono
|
||||||
|
temps_cumule += temps_paragraphe
|
||||||
|
|
||||||
|
# Conversion en minutes et secondes
|
||||||
|
minutes_paragraphe, secondes_paragraphe = divmod(temps_paragraphe, 60)
|
||||||
|
minutes_cumule, secondes_cumule = divmod(temps_cumule, 60)
|
||||||
|
|
||||||
print(f"{result}")
|
print(f"{result}")
|
||||||
results[i] = result
|
results[str(i)] = result
|
||||||
save_checkpoint(i, results) # Sauvegarde le checkpoint
|
save_checkpoint(i + batch_size - 1, results)
|
||||||
save_temp_results(results) # Sauvegarde les résultats temporaires
|
save_temp_results(results)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erreur : {e}")
|
print(f"Erreur : {e}")
|
||||||
continue
|
continue
|
||||||
|
print(f" Temps de traduction : {int(minutes_paragraphe)} min {secondes_paragraphe:.2f} sec")
|
||||||
|
print(f" Temps cumulé : {int(minutes_cumule)} min {secondes_cumule:.2f} sec")
|
||||||
|
|
||||||
|
|
||||||
# Génération des fichiers finaux
|
|
||||||
save_temp_results(results)
|
save_temp_results(results)
|
||||||
create_pdf_from_results(results, FINAL_OUTPUT_PDF)
|
create_pdf_from_results(results, FINAL_OUTPUT_PDF)
|
||||||
create_txt_from_results(results, FINAL_OUTPUT_TXT)
|
create_txt_from_results(results, FINAL_OUTPUT_TXT)
|
||||||
print("Traduction terminée !")
|
print("Traduction terminée !")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
1
llama.cpp
Submodule
1
llama.cpp
Submodule
Submodule llama.cpp added at e463bbdf65
@@ -1,8 +1,17 @@
|
|||||||
certifi==2026.1.4
|
certifi
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer
|
||||||
idna==3.11
|
idna
|
||||||
pillow==12.1.0
|
pillow
|
||||||
PyPDF2==3.0.1
|
PyPDF2
|
||||||
reportlab==4.4.7
|
reportlab
|
||||||
requests==2.32.5
|
requests
|
||||||
urllib3==2.6.2
|
urllib3
|
||||||
|
|
||||||
|
torch
|
||||||
|
transformers
|
||||||
|
datasets
|
||||||
|
peft
|
||||||
|
bitsandbytes
|
||||||
|
accelerate
|
||||||
|
trl
|
||||||
|
nltk
|
||||||
8
run.bat
8
run.bat
@@ -11,18 +11,19 @@ REM Chemin vers l'environnement virtuel Python (relatif au répertoire courant)
|
|||||||
set VENV_PATH=%CURRENT_DIR%\venv
|
set VENV_PATH=%CURRENT_DIR%\venv
|
||||||
|
|
||||||
REM Chemin vers votre script principal (relatif au répertoire courant)
|
REM Chemin vers votre script principal (relatif au répertoire courant)
|
||||||
set MAIN_SCRIPT_PATH=%CURRENT_DIR%\main.py
|
set MAIN_SCRIPT_PATH=%CURRENT_DIR%\Traduction\main.py
|
||||||
|
|
||||||
REM Activer l'environnement virtuel Python
|
REM Activer l'environnement virtuel Python
|
||||||
call %VENV_PATH%\Scripts\activate.bat
|
call %VENV_PATH%\Scripts\activate.bat
|
||||||
|
|
||||||
REM Lancer la compilation du modèle LLM pour Ollama
|
REM Lancer la compilation du modèle LLM pour Ollama
|
||||||
ollama create traductionUkrainienVersFrancais -f .\Modelfile
|
echo Compilation du modèle LLM pour Ollama
|
||||||
|
ollama create traductionUkrainienVersFrancais -f .\Traduction\Modelfile
|
||||||
|
|
||||||
:: 1. Vérifie si le processus ollama.exe est en cours d'exécution
|
:: 1. Vérifie si le processus ollama.exe est en cours d'exécution
|
||||||
tasklist | find "ollama.exe" >nul
|
tasklist | find "ollama.exe" >nul
|
||||||
if %ERRORLEVEL% equ 0 (
|
if %ERRORLEVEL% equ 0 (
|
||||||
echo [OK] Le processus Ollama est en cours d'exécution.
|
echo [OK] Le processus Ollama est bien en cours d'execution.
|
||||||
) else (
|
) else (
|
||||||
echo [ERREUR] Ollama n'est pas lancé.
|
echo [ERREUR] Ollama n'est pas lancé.
|
||||||
pause
|
pause
|
||||||
@@ -39,6 +40,7 @@ if %ERRORLEVEL% neq 0 (
|
|||||||
)
|
)
|
||||||
|
|
||||||
REM Exécuter le script principal
|
REM Exécuter le script principal
|
||||||
|
echo Lancement du script principal de traduction
|
||||||
python %MAIN_SCRIPT_PATH%
|
python %MAIN_SCRIPT_PATH%
|
||||||
|
|
||||||
endlocal
|
endlocal
|
||||||
Reference in New Issue
Block a user