import argparse
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
import numpy.typing as npt
import toml
from scipy.optimize import minimize_scalar
from scripts.optimize.utils import calculate_metrics, filter_outliers, load_dataset
from skimtoken import count_multilingual_simple
def extract_features(texts: list[str]) -> tuple[npt.NDArray[np.float64], list[str]]:
char_counts: list[float] = []
languages: list[str] = []
for text in texts:
char_count, language = count_multilingual_simple(text)
char_counts.append(char_count)
languages.append(language)
return np.array(char_counts), languages
def compute_error_rate(coefficient: float, char_counts: np.ndarray, y_true: np.ndarray) -> float:
y_pred = coefficient * char_counts
relative_errors = np.abs(y_true - y_pred) / y_true
error_rate = np.mean(relative_errors) return float(error_rate)
def optimize_language_coefficient(
char_counts: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
) -> float:
char_counts_filtered, y_filtered = filter_outliers(
char_counts.reshape(-1, 1), y, percentile=1.0
)
char_counts_filtered = char_counts_filtered.flatten()
result = minimize_scalar(
compute_error_rate,
args=(char_counts_filtered, y_filtered),
bounds=(-2.0, 2.0), method="bounded",
options={"xatol": 1e-8},
)
return float(result.x)
def optimize_parameters(
dataset_path: Path,
val_path: Path | None = None,
max_samples: int | None = None,
min_samples_per_lang: int = 10,
) -> dict[str, Any]:
print(f"Loading training dataset from {dataset_path}...")
data = load_dataset(dataset_path, max_samples)
print("Grouping samples by detected language...")
lang_data_by_detected: dict[str, list[dict[str, Any]]] = defaultdict(list)
for item in data:
detected_lang = item.get("detected_lang", "unknown")
lang_data_by_detected[detected_lang].append(item)
print(f"\nDetected languages in dataset: {list(lang_data_by_detected.keys())}")
print(f"Total samples: {len(data)}")
language_params: dict[str, dict[str, float]] = {}
for detected_lang, lang_samples in lang_data_by_detected.items():
if detected_lang == "unknown":
print(f"\nSkipping {detected_lang} language")
continue
if len(lang_samples) < min_samples_per_lang:
print(
f"\nSkipping {detected_lang}: only {len(lang_samples)} samples (min: {min_samples_per_lang})"
)
continue
print(f"\nProcessing {detected_lang} ({len(lang_samples)} samples)...")
texts = [item["text"] for item in lang_samples]
char_counts, _ = extract_features(texts)
y_lang = np.array([item["token_len"] for item in lang_samples])
optimized_coef = optimize_language_coefficient(char_counts, y_lang)
y_pred = optimized_coef * char_counts
lang_metrics = calculate_metrics(y_lang, y_pred)
print(f" Optimized coefficient: {optimized_coef:.6f}")
print(f" R²: {lang_metrics['r2']:.4f}")
print(f" RMSE: {lang_metrics['rmse']:.2f}")
print(f" Error rate (full): {lang_metrics['error_rate']:.1f}%")
print(f" Error rate (>5%): {lang_metrics['error_rate_5pct']:.1f}%")
language_params[detected_lang] = {
"coefficient": float(optimized_coef),
}
print("\nOptimizing default parameters (all languages)...")
all_texts = [item["text"] for item in data]
char_counts_all, _ = extract_features(all_texts)
y_train_all = np.array([item["token_len"] for item in data])
optimized_default_coef = optimize_language_coefficient(char_counts_all, y_train_all)
y_pred = optimized_default_coef * char_counts_all
default_metrics = calculate_metrics(y_train_all, y_pred)
print(f" Optimized coefficient: {optimized_default_coef:.6f}")
print(f" R²: {default_metrics['r2']:.4f}")
print(f" RMSE: {default_metrics['rmse']:.2f}")
print(f" Error rate (full): {default_metrics['error_rate']:.1f}%")
print(f" Error rate (>5%): {default_metrics['error_rate_5pct']:.1f}%")
default_params = {
"coefficient": float(optimized_default_coef),
}
if val_path and val_path.exists():
print("\n\nValidation Results:")
print("=" * 60)
print(f"Loading validation dataset from {val_path}...")
val_data = load_dataset(val_path)
print("Grouping validation samples by detected language...")
val_lang_data_by_detected: dict[str, list[dict[str, Any]]] = defaultdict(list)
for item in val_data:
detected_lang = item.get("detected_lang", "unknown")
val_lang_data_by_detected[detected_lang].append(item)
for detected_lang, val_lang_samples in val_lang_data_by_detected.items():
if detected_lang == "unknown" or len(val_lang_samples) < 5: continue
lang_code = detected_lang.lower()[:3].title()
if lang_code in language_params:
val_texts = [item["text"] for item in val_lang_samples]
char_counts_val, _ = extract_features(val_texts)
y_val_lang = np.array([item["token_len"] for item in val_lang_samples])
params = language_params[lang_code]
y_pred = params["coefficient"] * char_counts_val
val_lang_metrics = calculate_metrics(y_val_lang, y_pred)
print(f"\nValidation metrics for {detected_lang} ({len(y_val_lang)} samples):")
print(f" Coefficient: {params['coefficient']:.6f}")
print(f" R²: {val_lang_metrics['r2']:.4f}")
print(f" RMSE: {val_lang_metrics['rmse']:.2f}")
print(f" Error rate (full): {val_lang_metrics['error_rate']:.1f}%")
print(f" Error rate (>5%): {val_lang_metrics['error_rate_5pct']:.1f}%")
all_val_texts = [item["text"] for item in val_data]
char_counts_val_all, _ = extract_features(all_val_texts)
y_val_all = np.array([item["token_len"] for item in val_data])
y_pred = default_params["coefficient"] * char_counts_val_all
val_all_metrics = calculate_metrics(y_val_all, y_pred)
print("\nValidation metrics for default model (all languages):")
print(f" Coefficient: {default_params['coefficient']:.6f}")
print(f" R²: {val_all_metrics['r2']:.4f}")
print(f" RMSE: {val_all_metrics['rmse']:.2f}")
print(f" Error rate (full): {val_all_metrics['error_rate']:.1f}%")
print(f" Error rate (>5%): {val_all_metrics['error_rate_5pct']:.1f}%")
return {"default_params": default_params, "language_params": language_params}
def main() -> None:
parser = argparse.ArgumentParser(description="Optimize MultilingualSimpleMethod parameters")
parser.add_argument(
"--dataset",
type=Path,
default=Path("data/cc100_samples/train.jsonl"),
help="Path to training dataset JSONL file",
)
parser.add_argument(
"--val-dataset",
type=Path,
default=Path("data/cc100_samples/val.jsonl"),
help="Path to validation dataset JSONL file",
)
parser.add_argument(
"--max-samples", type=int, default=None, help="Maximum number of samples to use"
)
parser.add_argument(
"--min-samples-per-lang",
type=int,
default=10,
help="Minimum samples required to create language-specific parameters",
)
parser.add_argument(
"--output",
type=Path,
default=Path("params/multilingual_simple.toml"),
help="Output path for parameters",
)
args = parser.parse_args()
params = optimize_parameters(
args.dataset, args.val_dataset, args.max_samples, args.min_samples_per_lang
)
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w") as f:
toml.dump(params, f)
print(f"\nParameters saved to {args.output}")
if __name__ == "__main__":
main()